233 lines
12 KiB
JavaScript
233 lines
12 KiB
JavaScript
// @license http://www.gnu.org/licenses/agpl-3.0.html AGPL-3.0
|
|
let ffmpeg = null;
|
|
|
|
let loadingsvg = `<svg class="rotate" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g fill="none" fill-rule="evenodd"><path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z"/><path fill="currentColor" d="M12 4.5a7.5 7.5 0 1 0 0 15a7.5 7.5 0 0 0 0-15M1.5 12C1.5 6.201 6.201 1.5 12 1.5S22.5 6.201 22.5 12S17.799 22.5 12 22.5S1.5 17.799 1.5 12" opacity="0.1"/><path fill="currentColor" d="M12 4.5a7.46 7.46 0 0 0-5.187 2.083a1.5 1.5 0 0 1-2.075-2.166A10.46 10.46 0 0 1 12 1.5a1.5 1.5 0 0 1 0 3"/></g></svg>`;
|
|
let downloadsvg = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g fill="none"><path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z"/><path fill="currentColor" d="M20 15a1 1 0 0 1 1 1v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4a1 1 0 1 1 2 0v4h14v-4a1 1 0 0 1 1-1M12 2a1 1 0 0 1 1 1v10.243l2.536-2.536a1 1 0 1 1 1.414 1.414l-4.066 4.066a1.25 1.25 0 0 1-1.768 0L7.05 12.121a1 1 0 1 1 1.414-1.414L11 13.243V3a1 1 0 0 1 1-1"/></g></svg>`;
|
|
|
|
(function () {
|
|
if (Hls.isSupported()) {
|
|
|
|
var downloadsEnabled = document.cookie.split("; ").find((row) => row.startsWith("ffmpeg_video_downloads="))?.split("=")[1] == "on";
|
|
|
|
var videoSources = document.querySelectorAll("video source[type='application/vnd.apple.mpegurl']");
|
|
videoSources.forEach(function (source) {
|
|
var playlist = source.src;
|
|
|
|
var oldVideo = source.parentNode;
|
|
var autoplay = oldVideo.classList.contains("hls_autoplay");
|
|
|
|
// If HLS is supported natively then don't use hls.js
|
|
if (oldVideo.canPlayType(source.type) === "probably") {
|
|
if (autoplay) {
|
|
oldVideo.play();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Replace video with copy that will have all "source" elements removed
|
|
var newVideo = oldVideo.cloneNode(true);
|
|
var allSources = newVideo.querySelectorAll("source");
|
|
allSources.forEach(function (source) {
|
|
source.remove();
|
|
});
|
|
|
|
// Empty source to enable play event
|
|
newVideo.src = "about:blank";
|
|
|
|
oldVideo.parentNode.replaceChild(newVideo, oldVideo);
|
|
|
|
function initializeHls() {
|
|
newVideo.removeEventListener('play', initializeHls);
|
|
var hls = new Hls({ autoStartLoad: false });
|
|
hls.loadSource(playlist);
|
|
hls.attachMedia(newVideo);
|
|
hls.on(Hls.Events.MANIFEST_PARSED, function () {
|
|
hls.loadLevel = hls.levels.length - 1;
|
|
var availableLevels = hls.levels.map(function(level) {
|
|
return {
|
|
height: level.height,
|
|
width: level.width,
|
|
bitrate: level.bitrate,
|
|
};
|
|
});
|
|
|
|
addQualitySelector(newVideo, hls, availableLevels);
|
|
if (downloadsEnabled){ addVideoDownload(newVideo, hls); }
|
|
hls.startLoad();
|
|
newVideo.play();
|
|
});
|
|
|
|
hls.on(Hls.Events.ERROR, function (event, data) {
|
|
var errorType = data.type;
|
|
var errorFatal = data.fatal;
|
|
if (errorFatal) {
|
|
switch (errorType) {
|
|
case Hls.ErrorType.NETWORK_ERROR:
|
|
hls.startLoad();
|
|
break;
|
|
case Hls.ErrorType.MEDIA_ERROR:
|
|
hls.recoverMediaError();
|
|
break;
|
|
default:
|
|
hls.destroy();
|
|
break;
|
|
}
|
|
}
|
|
|
|
console.error("HLS error", data);
|
|
});
|
|
}
|
|
|
|
if (downloadsEnabled){
|
|
const { fetchFile } = FFmpegUtil;
|
|
const { FFmpeg } = FFmpegWASM;
|
|
|
|
function addVideoDownload(videoElement, hlsInstance) {
|
|
var mediaStream = [];
|
|
var downloadButton = document.createElement('button');
|
|
downloadButton.classList.add('video-options','download');
|
|
downloadButton.innerHTML = loadingsvg
|
|
const mergeStreams = async () => {
|
|
if (ffmpeg === null) {
|
|
ffmpeg = new FFmpeg();
|
|
await ffmpeg.load({
|
|
coreURL: "/ffmpeg/ffmpeg-core.js",
|
|
});
|
|
ffmpeg.on("log", ({ message }) => {
|
|
console.log(message); // This is quite noisy but i will include it
|
|
})
|
|
ffmpeg.on("progress", ({ progress, time }) => { // Progress TODO: show progress ring around button not just ⏳
|
|
// console.log("ffmpeg prog:",progress * 100)
|
|
});
|
|
}
|
|
// Combine Video Audio Streams
|
|
await ffmpeg.writeFile("video", await fetchFile(concatBlob(mediaStream['video'])));
|
|
await ffmpeg.writeFile("audio", await fetchFile(concatBlob(mediaStream['audio'])));
|
|
console.time('ffmpeg-exec');
|
|
await ffmpeg.exec(['-i', "video", '-i', "audio",'-c:v', "copy", '-c:a', "aac", 'output.mp4']);
|
|
console.timeEnd('ffmpeg-exec')
|
|
|
|
// Save
|
|
const toSlug = (str) => {
|
|
return str
|
|
.normalize('NFD')
|
|
.replace(/[\u0300-\u036f]/g, '')
|
|
.replace(/[\W_]+/g, '-')
|
|
.toLowerCase()
|
|
.replace(/^-+|-+$/g, '');
|
|
}
|
|
|
|
var filename = toSlug(videoElement.parentNode.parentNode.id || document.title)
|
|
const data = await ffmpeg.readFile('output.mp4');
|
|
saveAs(new Blob([data.buffer]),filename, {type: 'video/mp4'});
|
|
return
|
|
}
|
|
function saveAs(blob, filename) { // Yeah ok...
|
|
var url = URL.createObjectURL(blob);
|
|
var a = document.createElement("a");
|
|
document.body.appendChild(a);
|
|
a.style = "display: none";
|
|
a.href = url;
|
|
a.download = filename;
|
|
a.click();
|
|
window.URL.revokeObjectURL(url);
|
|
}
|
|
function concatBlob(inputArray) {
|
|
var totalLength = inputArray.reduce(function (prev, cur) {
|
|
return prev + cur.length
|
|
}, 0);
|
|
var result = new Uint8Array(totalLength);
|
|
var offset = 0;
|
|
inputArray.forEach(function (element) {
|
|
result.set(element, offset);
|
|
offset += element.length;
|
|
});
|
|
return new Blob([result], {
|
|
type: 'application/octet-stream'
|
|
});
|
|
}
|
|
function getStreams() {
|
|
var video = document.createElement('video');
|
|
video.autoplay = true;
|
|
var dataStreams = {
|
|
'video': [],
|
|
'audio': []
|
|
};
|
|
mediaStream = dataStreams; // Update stream
|
|
|
|
hlsInstance.on(Hls.Events.BUFFER_APPENDING, function (event, data) {
|
|
dataStreams[data.type].push(data.data);
|
|
});
|
|
var isDownloading = false
|
|
function startDownload() {
|
|
if (!isDownloading) { isDownloading = true } else { return }
|
|
downloadButton.innerHTML = loadingsvg
|
|
mergeStreams()
|
|
.then(_ => {
|
|
isDownloading = false
|
|
downloadButton.innerHTML = downloadsvg
|
|
});
|
|
}
|
|
|
|
function waitForLoad() {
|
|
const poll = resolve => {
|
|
if(hlsInstance._media.buffered.length === 1 &&
|
|
hlsInstance._media.buffered.start(0) === 0 &&
|
|
hlsInstance._media.buffered.end(0) === hlsInstance._media.duration)
|
|
resolve();
|
|
else setTimeout(_ => poll(resolve), 400);
|
|
}
|
|
return new Promise(poll);
|
|
}
|
|
|
|
waitForLoad(_ => flag === true)
|
|
.then(_ => {
|
|
downloadButton.innerHTML = downloadsvg
|
|
downloadButton.addEventListener('click', startDownload);
|
|
});
|
|
}
|
|
|
|
videoElement.parentNode.appendChild(downloadButton);
|
|
getStreams()
|
|
}
|
|
}
|
|
|
|
function addQualitySelector(videoElement, hlsInstance, availableLevels) {
|
|
var qualitySelector = document.createElement('select');
|
|
qualitySelector.classList.add('video-options');
|
|
var last = availableLevels.length - 1;
|
|
availableLevels.forEach(function (level, index) {
|
|
var option = document.createElement('option');
|
|
option.value = index.toString();
|
|
var bitrate = (level.bitrate / 1_000).toFixed(0);
|
|
option.text = level.height + 'p (' + bitrate + ' kbps)';
|
|
if (index === last) {
|
|
option.selected = "selected";
|
|
}
|
|
qualitySelector.appendChild(option);
|
|
});
|
|
qualitySelector.selectedIndex = availableLevels.length - 1;
|
|
qualitySelector.addEventListener('change', function () {
|
|
var selectedIndex = qualitySelector.selectedIndex;
|
|
hlsInstance.nextLevel = selectedIndex;
|
|
hlsInstance.startLoad();
|
|
});
|
|
|
|
videoElement.parentNode.appendChild(qualitySelector);
|
|
}
|
|
|
|
newVideo.addEventListener('play', initializeHls);
|
|
|
|
if (autoplay) {
|
|
newVideo.play();
|
|
}
|
|
});
|
|
} else {
|
|
var videos = document.querySelectorAll("video.hls_autoplay");
|
|
videos.forEach(function (video) {
|
|
video.setAttribute("autoplay", "");
|
|
});
|
|
}
|
|
})();
|
|
// @license-end
|