Removed ffmpeg downloading completely

This commit is contained in:
WardPearce
2025-01-24 15:10:39 +13:00
parent de6fd0a925
commit bc64e651bc
11 changed files with 12 additions and 467 deletions
+7 -6
View File
@@ -27,7 +27,6 @@
- Watch sync parties!
- Mini player.
- Silence skipper (Experimental.)
- ffmpeg integration for downloading videos with audio at any quality ([Configuration required](./docs/DOCKER.md#step-7-optional-enabling-downloads)).
- [YouTube.js](https://github.com/LuanRT/YouTube.js) fallback if Invidious fails loading videos for Desktop & Android.
- Preview video on hover.
- Sponsorblock built-in.
@@ -48,11 +47,13 @@
- YT path redirects (So your redirect plugins should still work!)
# Support table
| | Dash | HLS | Local video fallback | API-Extended | ffmpeg download merging | Dearrow | RYD |
|---------|------|-----|----------------------|--------------|-------------------------|---------|-----|
| Web | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ |
| Desktop | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ |
| Android | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ |
Heres the updated table with the "ffmpeg download merging" column completely removed:
| | Dash | HLS | Local video fallback | API-Extended | Dearrow | RYD |
|---------|------|-----|----------------------|--------------|---------|-----|
| Web | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ |
| Desktop | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Android | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
# Public instances
[Public instances are listed here](./docs/INSTANCES.md).
+2 -32
View File
@@ -1,12 +1,12 @@
{
"name": "materialious",
"version": "1.6.28",
"version": "1.6.30",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "materialious",
"version": "1.6.28",
"version": "1.6.30",
"dependencies": {
"@capacitor-community/electron": "^5.0.1",
"@capacitor/android": "^6.1.2",
@@ -18,7 +18,6 @@
"@capacitor/screen-orientation": "^6.0.2",
"@capacitor/status-bar": "^6.0.1",
"@capgo/inappbrowser": "^6.6.8",
"@ffmpeg/ffmpeg": "^0.12.10",
"@hugotomazi/capacitor-navigation-bar": "^4.0.1",
"@mediagrid/capacitor-native-audio": "^1.0.0",
"beercss": "^3.8.0",
@@ -45,7 +44,6 @@
},
"devDependencies": {
"@capacitor/assets": "^3.0.5",
"@ffmpeg/util": "^0.12.1",
"@sveltejs/adapter-auto": "^3.2.5",
"@sveltejs/adapter-static": "^3.0.5",
"@sveltejs/kit": "^2.9.1",
@@ -2084,34 +2082,6 @@
"node": ">=14"
}
},
"node_modules/@ffmpeg/ffmpeg": {
"version": "0.12.10",
"resolved": "https://registry.npmjs.org/@ffmpeg/ffmpeg/-/ffmpeg-0.12.10.tgz",
"integrity": "sha512-lVtk8PW8e+NUzGZhPTWj2P1J4/NyuCrbDD3O9IGpSeLYtUZKBqZO8CNj1WYGghep/MXoM8e1qVY1GztTkf8YYQ==",
"dependencies": {
"@ffmpeg/types": "^0.12.2"
},
"engines": {
"node": ">=18.x"
}
},
"node_modules/@ffmpeg/types": {
"version": "0.12.2",
"resolved": "https://registry.npmjs.org/@ffmpeg/types/-/types-0.12.2.tgz",
"integrity": "sha512-NJtxwPoLb60/z1Klv0ueshguWQ/7mNm106qdHkB4HL49LXszjhjCCiL+ldHJGQ9ai2Igx0s4F24ghigy//ERdA==",
"engines": {
"node": ">=16.x"
}
},
"node_modules/@ffmpeg/util": {
"version": "0.12.1",
"resolved": "https://registry.npmjs.org/@ffmpeg/util/-/util-0.12.1.tgz",
"integrity": "sha512-10jjfAKWaDyb8+nAkijcsi9wgz/y26LOc1NKJradNMyCIl6usQcBbhkjX5qhALrSBcOy6TOeksunTYa+a03qNQ==",
"dev": true,
"engines": {
"node": ">=18.x"
}
},
"node_modules/@floating-ui/core": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.7.tgz",
+1 -3
View File
@@ -13,7 +13,6 @@
},
"devDependencies": {
"@capacitor/assets": "^3.0.5",
"@ffmpeg/util": "^0.12.1",
"@sveltejs/adapter-auto": "^3.2.5",
"@sveltejs/adapter-static": "^3.0.5",
"@sveltejs/kit": "^2.9.1",
@@ -52,7 +51,6 @@
"@capacitor/screen-orientation": "^6.0.2",
"@capacitor/status-bar": "^6.0.1",
"@capgo/inappbrowser": "^6.6.8",
"@ffmpeg/ffmpeg": "^0.12.10",
"@hugotomazi/capacitor-navigation-bar": "^4.0.1",
"@mediagrid/capacitor-native-audio": "^1.0.0",
"beercss": "^3.8.0",
@@ -77,4 +75,4 @@
"vidstack": "^1.12.9",
"youtubei.js": "^12.2.0"
}
}
}
-178
View File
@@ -1,178 +0,0 @@
interface MediaQuality {
bandwidth: string;
videoUrl: string;
audioUrl: string;
resolution?: string;
frameRate?: string;
audioChannels?: string;
audioBandwidth?: string;
}
// https://github.com/ffmpegwasm/ffmpeg.wasm/issues/603#issuecomment-1982289892
// Ripped from https://github.com/ffmpegwasm/ffmpeg.wasm/blob/ae1cdac7db79c5315f9a1b716fcbd9bcfe27c902/packages/util/src/index.ts#L49
export async function fetchFile(
file: string,
onProgress?: (progress: number) => void
): Promise<Uint8Array> {
const response = await fetch(file);
const contentLength = response.headers.get('content-length');
// If content-length is missing, download without progress tracking
if (!contentLength) {
const data = await response.arrayBuffer();
return new Uint8Array(data);
}
const totalBytes = parseInt(contentLength, 10);
const data = new Uint8Array(totalBytes);
let position = 0;
let downloadBytes = 0;
const reader = response.body!.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
data.set(value, position);
position += value.length;
downloadBytes += value.byteLength;
if (onProgress) {
onProgress((downloadBytes / totalBytes) * 100);
}
}
return data;
}
export async function toBlobURL(
url: string,
mimeType: string,
onProgress?: (progress: number) => void
): Promise<string> {
return URL.createObjectURL(new Blob([await fetchFile(url, onProgress)], { type: mimeType }));
}
// Fetch and parse the DASH manifest
async function fetchDASHManifest(manifestUrl: string) {
const response = await fetch(manifestUrl);
const manifestText = await response.text();
const parser = new DOMParser();
const manifest = parser.parseFromString(manifestText, "text/xml");
return manifest;
}
export async function listCombinedQualities(manifestUrl: string): Promise<MediaQuality[]> {
const manifest = await fetchDASHManifest(`${manifestUrl}?local=true`);
const qualities: MediaQuality[] = [];
// Fetch all AdaptationSets (both video and audio)
const adaptationSets = manifest.getElementsByTagName('AdaptationSet');
let videoRepresentations: Element[] = [];
let audioRepresentations: Element[] = [];
// Separate video and audio AdaptationSets based on mimeType
for (let i = 0; i < adaptationSets.length; i++) {
const mimeType = adaptationSets[i].getAttribute('mimeType');
const representations = adaptationSets[i].getElementsByTagName('Representation');
if (mimeType && mimeType === 'video/mp4') {
videoRepresentations = [...videoRepresentations, ...Array.from(representations)];
} else if (mimeType && mimeType === 'audio/mp4') {
audioRepresentations = [...audioRepresentations, ...Array.from(representations)];
}
}
// Combine video and audio representations
videoRepresentations.forEach((videoRep) => {
const videoBandwidth = videoRep.getAttribute('bandwidth');
const videoUrl = videoRep.getElementsByTagName('BaseURL')[0].textContent as string;
const width = videoRep.getAttribute('width');
const height = videoRep.getAttribute('height');
const resolution = width && height ? `${width}x${height}` : undefined;
const frameRate = videoRep.getAttribute('frameRate') || '';
// Pair this video representation with each audio representation
audioRepresentations.forEach((audioRep) => {
const audioUrl = audioRep.getElementsByTagName('BaseURL')[0].textContent as string;
const audioChannels = audioRep.getElementsByTagName('AudioChannelConfiguration')[0]?.getAttribute('value') || '';
const audioBandwidth = audioRep.getAttribute('bandwidth') || '';
qualities.push({
bandwidth: videoBandwidth || '',
videoUrl,
audioUrl,
resolution,
frameRate,
audioChannels,
audioBandwidth
});
});
});
return qualities;
}
export async function mergeMediaFromDASH(
selectedQuality: MediaQuality,
fileName: string,
progressCallbacks?: {
video?: (progress: number) => void,
audio?: (progress: number) => void,
merging?: (progress: number) => void,
loadingFfmpeg?: (completed: boolean) => void;
classWorker?: (progress: number) => void,
core?: (progress: number) => void;
wasm?: (progress: number) => void;
worker?: (progress: number) => void;
}
) {
if (progressCallbacks?.loadingFfmpeg) {
progressCallbacks.loadingFfmpeg(false);
}
const FFmpeg = await import('@ffmpeg/ffmpeg');
const ffmpeg = new FFmpeg.FFmpeg();
ffmpeg.on('progress', ({ progress }) => {
if (progressCallbacks?.merging) {
progressCallbacks.merging(progress * 100);
}
});
await ffmpeg.load({
classWorkerURL: await toBlobURL('/ffmpeg/ffmpeg-worker.js', 'text/javascript', progressCallbacks?.classWorker),
coreURL: await toBlobURL('/ffmpeg/ffmpeg-core.js', 'text/javascript', progressCallbacks?.core),
wasmURL: await toBlobURL('/ffmpeg/ffmpeg-core.wasm', 'application/wasm', progressCallbacks?.wasm),
workerURL: await toBlobURL('/ffmpeg/ffmpeg-core.worker.js', 'text/javascript', progressCallbacks?.worker)
});
if (progressCallbacks?.loadingFfmpeg) {
progressCallbacks.loadingFfmpeg(true);
}
const { videoUrl, audioUrl } = selectedQuality;
ffmpeg.writeFile('input.mp4', await fetchFile(videoUrl, progressCallbacks?.video));
ffmpeg.writeFile('input.mp3', await fetchFile(audioUrl, progressCallbacks?.audio));
await ffmpeg.exec(['-i', 'input.mp4', '-i', 'input.mp3', '-c:v', 'copy', '-c:a', 'aac', 'output.mp4']);
const data = await ffmpeg.readFile('output.mp4');
const mergedFile = new Blob([data], { type: 'video/mp4' });
const url = URL.createObjectURL(mergedFile);
const a = document.createElement('a');
a.href = url;
a.download = `${fileName} - ${selectedQuality.resolution}.mp4`;
document.body.appendChild(a);
a.click();
URL.revokeObjectURL(url);
document.body.removeChild(a);
}
+2 -2
View File
@@ -321,9 +321,9 @@
{@html $_('invidiousBlockWarning', {
values: {
android:
'<a href="https://github.com/Materialious/Materialious/releases/latest" target="_blank" class="link" rel="noopener noreferrer">Android</a>',
'<a href="https://github.com/Materialious/Materialious?tab=readme-ov-file#android" target="_blank" class="link" rel="noopener noreferrer">Android</a>',
desktop:
'<a href="https://github.com/Materialious/Materialious/releases/latest" target="_blank" class="link" rel="noopener noreferrer">Desktop</a>'
'<a href="https://github.com/Materialious/Materialious?tab=readme-ov-file#desktop-windowsmacoslinux" target="_blank" class="link" rel="noopener noreferrer">Desktop</a>'
}
})}
</p>
@@ -41,7 +41,6 @@
} from '$lib/store';
import Thumbnail from '$lib/Thumbnail.svelte';
import Transcript from '$lib/Transcript.svelte';
import { mergeMediaFromDASH } from '$lib/videoDownload';
import ui from 'beercss';
import type { DataConnection } from 'peerjs';
import { type Segment } from 'sponsorblock-api';
@@ -434,50 +433,6 @@
let downloadStage: string | undefined;
let downloadProgress: number = 0;
function onVideoDownloadProgress(progress: number) {
downloadStage = $_('player.downloadSteps.video');
downloadProgress = progress;
}
function onAudioDownloadProgress(progress: number) {
downloadStage = $_('player.downloadSteps.audio');
downloadProgress = progress;
}
function onMergingProgress(progress: number) {
downloadStage = $_('player.downloadSteps.merging');
downloadProgress = progress;
if (progress >= 100) {
downloadStage = undefined;
}
}
function onLoadingFFmpeg(completed: boolean) {
downloadProgress = 0;
downloadStage = $_('player.downloadSteps.ffmpeg');
}
function onClassWorkerProgress(progress: number) {
downloadStage = $_('player.downloadSteps.classWorker');
downloadProgress = progress;
}
function onCoreProgress(progress: number) {
downloadStage = $_('player.downloadSteps.core');
downloadProgress = progress;
}
function onWasmProgress(progress: number) {
downloadStage = $_('player.downloadSteps.wasm');
downloadProgress = progress;
}
function onWorkerProgress(progress: number) {
downloadStage = $_('player.downloadSteps.worker');
downloadProgress = progress;
}
</script>
<svelte:head>
@@ -621,33 +576,6 @@
<ShareVideo video={data.video} />
</menu></button
>
{#await data.streamed.downloadQualitiesDash then downloadQualitiesDash}
{#if downloadQualitiesDash}
<button class="border"
><i>download</i>
<div class="tooltip">{$_('player.download')}</div>
<menu class="no-wrap">
{#each downloadQualitiesDash as quality}
<a
class="row"
href="#download"
on:click={async () =>
await mergeMediaFromDASH(quality, data.video.title, {
video: onVideoDownloadProgress,
audio: onAudioDownloadProgress,
merging: onMergingProgress,
loadingFfmpeg: onLoadingFFmpeg,
classWorker: onClassWorkerProgress,
core: onCoreProgress,
wasm: onWasmProgress,
worker: onWorkerProgress
})}>{quality.resolution}</a
>
{/each}
</menu></button
>
{/if}
{/await}
{#if personalPlaylists}
<button class="border">
<i>add</i>
@@ -13,7 +13,6 @@ import {
returnYTDislikesInstanceStore,
returnYtDislikesStore
} from '$lib/store';
import { listCombinedQualities } from '$lib/videoDownload';
import { error } from '@sveltejs/kit';
import { get } from 'svelte/store';
@@ -51,15 +50,6 @@ export async function load({ params, url }) {
}
let downloadQualitiesDash;
if (
import.meta.env.VITE_DEFAULT_DOWNLOAD_ENABLED &&
import.meta.env.VITE_DEFAULT_DOWNLOAD_ENABLED.toString().toLowerCase() === 'true' &&
!video.liveNow
) {
downloadQualitiesDash = listCombinedQualities(video.dashUrl);
}
return {
video: video,
content: phaseDescription(video.descriptionHtml, video.fallbackPatch === 'youtubejs'),
@@ -69,7 +59,6 @@ export async function load({ params, url }) {
returnYTDislikes: returnYTDislikes,
comments: comments,
subscribed: amSubscribed(video.authorId),
downloadQualitiesDash: downloadQualitiesDash
}
};
}
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -1 +0,0 @@
"use strict";var Module={};var ENVIRONMENT_IS_NODE=typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string";if(ENVIRONMENT_IS_NODE){var nodeWorkerThreads=require("worker_threads");var parentPort=nodeWorkerThreads.parentPort;parentPort.on("message",data=>onmessage({data:data}));var fs=require("fs");Object.assign(global,{self:global,require:require,Module:Module,location:{href:__filename},Worker:nodeWorkerThreads.Worker,importScripts:function(f){(0,eval)(fs.readFileSync(f,"utf8")+"//# sourceURL="+f)},postMessage:function(msg){parentPort.postMessage(msg)},performance:global.performance||{now:function(){return Date.now()}}})}var initializedJS=false;function threadPrintErr(){var text=Array.prototype.slice.call(arguments).join(" ");if(ENVIRONMENT_IS_NODE){fs.writeSync(2,text+"\n");return}console.error(text)}function threadAlert(){var text=Array.prototype.slice.call(arguments).join(" ");postMessage({cmd:"alert",text:text,threadId:Module["_pthread_self"]()})}var err=threadPrintErr;self.alert=threadAlert;Module["instantiateWasm"]=(info,receiveInstance)=>{var module=Module["wasmModule"];Module["wasmModule"]=null;var instance=new WebAssembly.Instance(module,info);return receiveInstance(instance)};self.onunhandledrejection=e=>{throw e.reason??e};function handleMessage(e){try{if(e.data.cmd==="load"){let messageQueue=[];self.onmessage=e=>messageQueue.push(e);self.startWorker=instance=>{Module=instance;postMessage({"cmd":"loaded"});for(let msg of messageQueue){handleMessage(msg)}self.onmessage=handleMessage};Module["wasmModule"]=e.data.wasmModule;for(const handler of e.data.handlers){Module[handler]=function(){postMessage({cmd:"callHandler",handler:handler,args:[...arguments]})}}Module["wasmMemory"]=e.data.wasmMemory;Module["buffer"]=Module["wasmMemory"].buffer;Module["ENVIRONMENT_IS_PTHREAD"]=true;(e.data.urlOrBlob?import(e.data.urlOrBlob):import("./ffmpeg-core.js")).then(exports=>exports.default(Module))}else if(e.data.cmd==="run"){Module["__emscripten_thread_init"](e.data.pthread_ptr,0,0,1);Module["__emscripten_thread_mailbox_await"](e.data.pthread_ptr);Module["establishStackSpace"]();Module["PThread"].receiveObjectTransfer(e.data);Module["PThread"].threadInitTLS();if(!initializedJS){initializedJS=true}try{Module["invokeEntryPoint"](e.data.start_routine,e.data.arg)}catch(ex){if(ex!="unwind"){throw ex}}}else if(e.data.cmd==="cancel"){if(Module["_pthread_self"]()){Module["__emscripten_thread_exit"](-1)}}else if(e.data.target==="setimmediate"){}else if(e.data.cmd==="checkMailbox"){if(initializedJS){Module["checkMailbox"]()}}else if(e.data.cmd){err("worker.js received unknown command "+e.data.cmd);err(e.data)}}catch(ex){if(Module["__emscripten_thread_crashed"]){Module["__emscripten_thread_crashed"]()}throw ex}}self.onmessage=handleMessage;
-146
View File
@@ -1,146 +0,0 @@
(function () {
"use strict";
const R = "./ffmpeg-core.js";
var E;
(function (t) {
t.LOAD = "LOAD", t.EXEC = "EXEC", t.WRITE_FILE = "WRITE_FILE", t.READ_FILE = "READ_FILE", t.DELETE_FILE = "DELETE_FILE", t.RENAME = "RENAME", t.CREATE_DIR = "CREATE_DIR", t.LIST_DIR = "LIST_DIR", t.DELETE_DIR = "DELETE_DIR", t.ERROR = "ERROR", t.DOWNLOAD = "DOWNLOAD", t.PROGRESS = "PROGRESS", t.LOG = "LOG", t.MOUNT = "MOUNT", t.UNMOUNT = "UNMOUNT"
})(E || (E = {}));
const a = new Error("unknown message type"),
f = new Error("ffmpeg is not loaded, call `await ffmpeg.load()` first"),
u = new Error("failed to import ffmpeg-core.js");
let r;
const O = async ({
coreURL: t,
wasmURL: n,
workerURL: e
}) => {
const o = !r;
try {
t || (t = R), importScripts(t)
} catch {
if (t || (t = R.replace("/umd/", "/esm/")), self.createFFmpegCore = (await import(t)).default, !self.createFFmpegCore) throw u
}
const s = t,
c = n || t.replace(/.js$/g, ".wasm"),
b = e || t.replace(/.js$/g, ".worker.js");
return r = await self.createFFmpegCore({
mainScriptUrlOrBlob: `${s}#${btoa(JSON.stringify({ wasmURL: c, workerURL: b }))}`
}), r.setLogger(i => self.postMessage({
type: E.LOG,
data: i
})), r.setProgress(i => self.postMessage({
type: E.PROGRESS,
data: i
})), o
}, l = ({
args: t,
timeout: n = -1
}) => {
r.setTimeout(n), r.exec(...t);
const e = r.ret;
return r.reset(), e
}, m = ({
path: t,
data: n
}) => (r.FS.writeFile(t, n), !0), D = ({
path: t,
encoding: n
}) => r.FS.readFile(t, {
encoding: n
}), S = ({
path: t
}) => (r.FS.unlink(t), !0), I = ({
oldPath: t,
newPath: n
}) => (r.FS.rename(t, n), !0), L = ({
path: t
}) => (r.FS.mkdir(t), !0), N = ({
path: t
}) => {
const n = r.FS.readdir(t),
e = [];
for (const o of n) {
const s = r.FS.stat(`${t}/${o}`),
c = r.FS.isDir(s.mode);
e.push({
name: o,
isDir: c
})
}
return e
}, A = ({
path: t
}) => (r.FS.rmdir(t), !0), w = ({
fsType: t,
options: n,
mountPoint: e
}) => {
const o = t,
s = r.FS.filesystems[o];
return s ? (r.FS.mount(s, n, e), !0) : !1
}, k = ({
mountPoint: t
}) => (r.FS.unmount(t), !0);
self.onmessage = async ({
data: {
id: t,
type: n,
data: e
}
}) => {
const o = [];
let s;
try {
if (n !== E.LOAD && !r) throw f;
switch (n) {
case E.LOAD:
s = await O(e);
break;
case E.EXEC:
s = l(e);
break;
case E.WRITE_FILE:
s = m(e);
break;
case E.READ_FILE:
s = D(e);
break;
case E.DELETE_FILE:
s = S(e);
break;
case E.RENAME:
s = I(e);
break;
case E.CREATE_DIR:
s = L(e);
break;
case E.LIST_DIR:
s = N(e);
break;
case E.DELETE_DIR:
s = A(e);
break;
case E.MOUNT:
s = w(e);
break;
case E.UNMOUNT:
s = k(e);
break;
default:
throw a
}
} catch (c) {
self.postMessage({
id: t,
type: E.ERROR,
data: c.toString()
});
return
}
s instanceof Uint8Array && o.push(s.buffer), self.postMessage({
id: t,
type: n,
data: s
}, o)
}
})();