2024-06-23 19:57:23 +12:00
|
|
|
// @license http://www.gnu.org/licenses/agpl-3.0.html AGPL-3.0
|
|
|
|
let ffmpeg = null;
|
|
|
|
(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.innerText = "⏳"
|
|
|
|
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');
|
2024-08-31 10:18:05 +12:00
|
|
|
saveAs(new Blob([data.buffer]),filename, {type: 'video/mp4'});
|
2024-06-23 19:57:23 +12:00
|
|
|
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.innerText = "⏳"
|
|
|
|
mergeStreams()
|
|
|
|
.then(_ => {
|
|
|
|
isDownloading = false
|
|
|
|
downloadButton.innerText = "⭳"
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
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.innerText = "⭳"
|
|
|
|
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
|