mirror of
https://github.com/imputnet/cobalt.git
synced 2026-10-25 03:22:19 +00:00
Merge 2e8c384def into a636575b09
This commit is contained in:
commit
ed0fd48375
7 changed files with 63 additions and 44 deletions
|
|
@ -6,7 +6,7 @@
|
|||
"exports": "./src/cobalt.js",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
"node": ">=20"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node src/cobalt",
|
||||
|
|
@ -40,7 +40,7 @@
|
|||
"set-cookie-parser": "2.7.2",
|
||||
"undici": "^6.24.1",
|
||||
"url-pattern": "1.0.3",
|
||||
"youtubei.js": "17.0.1",
|
||||
"youtubei.js": "18.1.0",
|
||||
"zod": "^4.2.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ Platform.shim.eval = async (data) => {
|
|||
}
|
||||
|
||||
const PLAYER_REFRESH_PERIOD = 1000 * 60 * 15; // ms
|
||||
|
||||
let innertube, lastRefreshedAt;
|
||||
|
||||
const codecList = {
|
||||
|
|
@ -57,7 +56,10 @@ const hlsCodecList = {
|
|||
}
|
||||
}
|
||||
|
||||
const clientsWithNoCipher = ['IOS', 'ANDROID', 'YTSTUDIO_ANDROID', 'YTMUSIC_ANDROID'];
|
||||
const clientsWithNoCipher = [
|
||||
'IOS', 'ANDROID', 'ANDROID_VR', 'VISIONOS',
|
||||
'YTSTUDIO_ANDROID', 'YTMUSIC_ANDROID'
|
||||
];
|
||||
|
||||
const videoQualities = [144, 240, 360, 480, 720, 1080, 1440, 2160, 4320];
|
||||
|
||||
|
|
|
|||
|
|
@ -87,7 +87,8 @@ const render = async (res, streamInfo, ffargs, estimateMultiplier) => {
|
|||
await estimateTunnelLength(streamInfo, estimateMultiplier)
|
||||
);
|
||||
|
||||
pipe(muxOutput, res, shutdown);
|
||||
// Keep the response open until shutdown can report an empty ffmpeg output as an error.
|
||||
pipe(muxOutput, res, shutdown, { end: false });
|
||||
|
||||
process.on('close', shutdown);
|
||||
res.on('finish', shutdown);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { request } from "undici";
|
||||
import { Readable } from "node:stream";
|
||||
import { closeRequest, getHeaders, pipe } from "./shared.js";
|
||||
import { closeRequest, closeResponse, getHeaders, pipe } from "./shared.js";
|
||||
import { handleHlsPlaylist, isHlsResponse, probeInternalHLSTunnel } from "./internal-hls.js";
|
||||
|
||||
const CHUNK_SIZE = BigInt(8e6); // 8 MB
|
||||
|
|
@ -9,31 +9,37 @@ const min = (a, b) => a < b ? a : b;
|
|||
const serviceNeedsChunks = new Set(["youtube", "vk"]);
|
||||
|
||||
async function* readChunks(streamInfo, size) {
|
||||
let read = 0n, chunksSinceTransplant = 0;
|
||||
let read = 0n;
|
||||
while (read < size) {
|
||||
if (streamInfo.controller.signal.aborted) {
|
||||
throw new Error("controller aborted");
|
||||
}
|
||||
|
||||
const chunk = await request(streamInfo.url, {
|
||||
headers: {
|
||||
...getHeaders(streamInfo.service),
|
||||
Range: `bytes=${read}-${read + CHUNK_SIZE}`
|
||||
},
|
||||
dispatcher: streamInfo.dispatcher,
|
||||
signal: streamInfo.controller.signal,
|
||||
maxRedirections: 4
|
||||
});
|
||||
let chunk, attempts = 0;
|
||||
while (true) {
|
||||
chunk = await request(streamInfo.url, {
|
||||
headers: {
|
||||
...getHeaders(streamInfo.service),
|
||||
Range: `bytes=${read}-${read + CHUNK_SIZE}`
|
||||
},
|
||||
dispatcher: streamInfo.dispatcher,
|
||||
signal: streamInfo.controller.signal,
|
||||
maxRedirections: 4
|
||||
});
|
||||
|
||||
if (chunk.statusCode === 403 && chunksSinceTransplant >= 3 && streamInfo.transplant) {
|
||||
chunksSinceTransplant = 0;
|
||||
try {
|
||||
if (chunk.statusCode === 403 && streamInfo.transplant && attempts++ < 3) {
|
||||
await chunk.body.dump();
|
||||
await streamInfo.transplant(streamInfo.dispatcher);
|
||||
continue;
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
chunksSinceTransplant++;
|
||||
if (chunk.statusCode !== 200 && chunk.statusCode !== 206) {
|
||||
await chunk.body.dump();
|
||||
throw new Error(`media request failed with status ${chunk.statusCode}`);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
const expected = min(CHUNK_SIZE, size - read);
|
||||
const received = BigInt(chunk.headers['content-length']);
|
||||
|
|
@ -52,11 +58,18 @@ async function* readChunks(streamInfo, size) {
|
|||
|
||||
async function handleChunkedStream(streamInfo, res) {
|
||||
const { signal } = streamInfo.controller;
|
||||
const cleanup = () => (res.end(), closeRequest(streamInfo.controller));
|
||||
const cleanup = () => {
|
||||
if (!res.headersSent) {
|
||||
res.removeHeader('content-length');
|
||||
res.removeHeader('content-type');
|
||||
}
|
||||
closeResponse(res);
|
||||
closeRequest(streamInfo.controller);
|
||||
};
|
||||
|
||||
try {
|
||||
let req, attempts = 3;
|
||||
while (attempts--) {
|
||||
let req;
|
||||
for (let attempts = 0; ; attempts++) {
|
||||
req = await fetch(streamInfo.url, {
|
||||
headers: getHeaders(streamInfo.service),
|
||||
method: 'HEAD',
|
||||
|
|
@ -65,13 +78,8 @@ async function handleChunkedStream(streamInfo, res) {
|
|||
});
|
||||
|
||||
streamInfo.url = req.url;
|
||||
if (req.status === 403 && streamInfo.transplant) {
|
||||
try {
|
||||
await streamInfo.transplant(streamInfo.dispatcher);
|
||||
} catch {
|
||||
break;
|
||||
}
|
||||
} else break;
|
||||
if (req.status !== 403 || !streamInfo.transplant || attempts >= 3) break;
|
||||
await streamInfo.transplant(streamInfo.dispatcher);
|
||||
}
|
||||
|
||||
const size = BigInt(req.headers.get('content-length'));
|
||||
|
|
|
|||
|
|
@ -43,14 +43,14 @@ export function getHeaders(service) {
|
|||
.reduce((p, [key, val]) => ({ ...p, [key]: String(val) }), {})
|
||||
}
|
||||
|
||||
export function pipe(from, to, done) {
|
||||
export function pipe(from, to, done, options) {
|
||||
from.on('error', done)
|
||||
.on('close', done);
|
||||
|
||||
to.on('error', done)
|
||||
.on('close', done);
|
||||
|
||||
from.pipe(to);
|
||||
from.pipe(to, options);
|
||||
}
|
||||
|
||||
export async function estimateTunnelLength(streamInfo, multiplier = 1.1) {
|
||||
|
|
|
|||
|
|
@ -260,6 +260,8 @@ the value is a number, either `0` or `1`.
|
|||
### CUSTOM_INNERTUBE_CLIENT
|
||||
innertube client that will be used instead of the default one.
|
||||
|
||||
`VISIONOS` is available as an alternative for videos whose media URLs are rejected with the default client. It does not work for every video (for example, made-for-kids videos). The default remains `IOS`.
|
||||
|
||||
the value is a string.
|
||||
|
||||
### YOUTUBE_SESSION_SERVER
|
||||
|
|
|
|||
26
pnpm-lock.yaml
generated
26
pnpm-lock.yaml
generated
|
|
@ -62,8 +62,8 @@ importers:
|
|||
specifier: 1.0.3
|
||||
version: 1.0.3
|
||||
youtubei.js:
|
||||
specifier: 17.0.1
|
||||
version: 17.0.1
|
||||
specifier: 18.1.0
|
||||
version: 18.1.0
|
||||
zod:
|
||||
specifier: ^4.2.1
|
||||
version: 4.3.6
|
||||
|
|
@ -1322,6 +1322,9 @@ packages:
|
|||
picomatch:
|
||||
optional: true
|
||||
|
||||
fflate@0.8.3:
|
||||
resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==}
|
||||
|
||||
ffmpeg-static@5.3.0:
|
||||
resolution: {integrity: sha512-H+K6sW6TiIX6VGend0KQwthe+kaceeH/luE8dIZyOP35ik7ahYojDuqlTV1bOrtEwl01sy2HFNGQfi5IDJvotg==}
|
||||
engines: {node: '>=16'}
|
||||
|
|
@ -1645,9 +1648,9 @@ packages:
|
|||
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
meriyah@6.1.4:
|
||||
resolution: {integrity: sha512-Sz8FzjzI0kN13GK/6MVEsVzMZEPvOhnmmI1lU5+/1cGOiK3QUahntrNNtdVeihrO7t9JpoH75iMNXg6R6uWflQ==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
meriyah@7.3.3:
|
||||
resolution: {integrity: sha512-uE5cnoNj+UYhoMdZDuymCzr5TzEuE3ZF2C4kn6Z76rLhkwnvZ/+6dUZnM41T3fifyuhW0+n8vK7eq6DKJxUfPA==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
methods@1.1.2:
|
||||
resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==}
|
||||
|
|
@ -2268,8 +2271,8 @@ packages:
|
|||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
youtubei.js@17.0.1:
|
||||
resolution: {integrity: sha512-1lO4b8UqMDzE0oh2qEGzbBOd4UYRdxn/4PdpRM7BGTHxM6ddsEsKZTu90jp8V9FHVgC2h1UirQyqoqLiKwl+Zg==}
|
||||
youtubei.js@18.1.0:
|
||||
resolution: {integrity: sha512-4Goo3ZeO6/kE8Mq67eC8Z9K1rijeqRwyKxf/YM4lqf8bB4r/s8JC0V3YFx7+l/AJR3LnAsulTsiIj+AFe0cduA==}
|
||||
|
||||
zimmerframe@1.1.2:
|
||||
resolution: {integrity: sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==}
|
||||
|
|
@ -3286,6 +3289,8 @@ snapshots:
|
|||
optionalDependencies:
|
||||
picomatch: 4.0.4
|
||||
|
||||
fflate@0.8.3: {}
|
||||
|
||||
ffmpeg-static@5.3.0:
|
||||
dependencies:
|
||||
'@derhuerst/http-basic': 8.2.4
|
||||
|
|
@ -3580,7 +3585,7 @@ snapshots:
|
|||
|
||||
merge2@1.4.1: {}
|
||||
|
||||
meriyah@6.1.4: {}
|
||||
meriyah@7.3.3: {}
|
||||
|
||||
methods@1.1.2: {}
|
||||
|
||||
|
|
@ -4147,10 +4152,11 @@ snapshots:
|
|||
|
||||
yocto-queue@0.1.0: {}
|
||||
|
||||
youtubei.js@17.0.1:
|
||||
youtubei.js@18.1.0:
|
||||
dependencies:
|
||||
'@bufbuild/protobuf': 2.2.5
|
||||
meriyah: 6.1.4
|
||||
fflate: 0.8.3
|
||||
meriyah: 7.3.3
|
||||
|
||||
zimmerframe@1.1.2: {}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue