Added Seek Support
This commit is contained in:
parent
5a094be82e
commit
f2598c9a7c
17
package-lock.json
generated
17
package-lock.json
generated
@ -1,13 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "play-dl",
|
"name": "play-dl",
|
||||||
"version": "1.4.5",
|
"version": "1.5.0",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "play-dl",
|
"name": "play-dl",
|
||||||
"version": "1.4.5",
|
"version": "1.5.0",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
|
"dependencies": {
|
||||||
|
"play-audio": "^0.4.3"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^16.9.4",
|
"@types/node": "^16.9.4",
|
||||||
"prettier": "^2.3.1",
|
"prettier": "^2.3.1",
|
||||||
@ -162,6 +165,11 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/play-audio": {
|
||||||
|
"version": "0.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/play-audio/-/play-audio-0.4.3.tgz",
|
||||||
|
"integrity": "sha512-DOLTP1+cgXH0k1ZdZyXXRsAPnVrzV2xZV6EXpWRsMtk24oolS7mD3WUQltuCeuJXKGM1tIsXLr+EZo6Ky4aKRg=="
|
||||||
|
},
|
||||||
"node_modules/prettier": {
|
"node_modules/prettier": {
|
||||||
"version": "2.5.0",
|
"version": "2.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.5.0.tgz",
|
||||||
@ -382,6 +390,11 @@
|
|||||||
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
|
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"play-audio": {
|
||||||
|
"version": "0.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/play-audio/-/play-audio-0.4.3.tgz",
|
||||||
|
"integrity": "sha512-DOLTP1+cgXH0k1ZdZyXXRsAPnVrzV2xZV6EXpWRsMtk24oolS7mD3WUQltuCeuJXKGM1tIsXLr+EZo6Ky4aKRg=="
|
||||||
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"version": "2.5.0",
|
"version": "2.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.5.0.tgz",
|
||||||
|
|||||||
@ -44,8 +44,11 @@
|
|||||||
"@types/node": "^16.9.4",
|
"@types/node": "^16.9.4",
|
||||||
"prettier": "^2.3.1",
|
"prettier": "^2.3.1",
|
||||||
"typedoc": "^0.22.9",
|
"typedoc": "^0.22.9",
|
||||||
|
"typedoc-plugin-extras": "^2.2.1",
|
||||||
"typedoc-plugin-missing-exports": "^0.22.4",
|
"typedoc-plugin-missing-exports": "^0.22.4",
|
||||||
"typescript": "^4.4.4",
|
"typescript": "^4.4.4"
|
||||||
"typedoc-plugin-extras": "^2.2.1"
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"play-audio": "^0.4.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
217
play-dl/YouTube/classes/SeekStream.ts
Normal file
217
play-dl/YouTube/classes/SeekStream.ts
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
import { IncomingMessage } from "http";
|
||||||
|
import { Readable } from "stream";
|
||||||
|
import { request_stream } from "../../Request";
|
||||||
|
import { parseAudioFormats, StreamOptions, StreamType } from "../stream";
|
||||||
|
import { video_info } from "../utils";
|
||||||
|
import { Timer } from "./LiveStream";
|
||||||
|
import { WebmSeeker, WebmSeekerState } from "./WebmSeeker";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* YouTube Stream Class for playing audio from normal videos.
|
||||||
|
*/
|
||||||
|
export class SeekStream {
|
||||||
|
/**
|
||||||
|
* Readable Stream through which data passes
|
||||||
|
*/
|
||||||
|
stream: WebmSeeker;
|
||||||
|
/**
|
||||||
|
* Type of audio data that we recieved from normal youtube url.
|
||||||
|
*/
|
||||||
|
type: StreamType;
|
||||||
|
/**
|
||||||
|
* Audio Endpoint Format Url to get data from.
|
||||||
|
*/
|
||||||
|
private url: string;
|
||||||
|
/**
|
||||||
|
* Used to calculate no of bytes data that we have recieved
|
||||||
|
*/
|
||||||
|
private bytes_count: number;
|
||||||
|
/**
|
||||||
|
* Calculate per second bytes by using contentLength (Total bytes) / Duration (in seconds)
|
||||||
|
*/
|
||||||
|
private per_sec_bytes: number;
|
||||||
|
/**
|
||||||
|
* Total length of audio file in bytes
|
||||||
|
*/
|
||||||
|
private content_length: number;
|
||||||
|
/**
|
||||||
|
* YouTube video url. [ Used only for retrying purposes only. ]
|
||||||
|
*/
|
||||||
|
private video_url: string;
|
||||||
|
/**
|
||||||
|
* Timer for looping data every 265 seconds.
|
||||||
|
*/
|
||||||
|
private timer: Timer;
|
||||||
|
/**
|
||||||
|
* Quality given by user. [ Used only for retrying purposes only. ]
|
||||||
|
*/
|
||||||
|
private quality: number;
|
||||||
|
/**
|
||||||
|
* Incoming message that we recieve.
|
||||||
|
*
|
||||||
|
* Storing this is essential.
|
||||||
|
* This helps to destroy the TCP connection completely if you stopped player in between the stream
|
||||||
|
*/
|
||||||
|
private request: IncomingMessage | null;
|
||||||
|
/**
|
||||||
|
* YouTube Stream Class constructor
|
||||||
|
* @param url Audio Endpoint url.
|
||||||
|
* @param type Type of Stream
|
||||||
|
* @param duration Duration of audio playback [ in seconds ]
|
||||||
|
* @param contentLength Total length of Audio file in bytes.
|
||||||
|
* @param video_url YouTube video url.
|
||||||
|
* @param options Options provided to stream function.
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
url: string,
|
||||||
|
duration: number,
|
||||||
|
contentLength: number,
|
||||||
|
video_url: string,
|
||||||
|
options: StreamOptions
|
||||||
|
) {
|
||||||
|
this.stream = new WebmSeeker({ highWaterMark: 5 * 1000 * 1000, readableObjectMode : true });
|
||||||
|
this.url = url;
|
||||||
|
this.quality = options.quality as number;
|
||||||
|
this.type = StreamType.Opus;
|
||||||
|
this.bytes_count = 0;
|
||||||
|
this.video_url = video_url;
|
||||||
|
this.per_sec_bytes = Math.ceil(contentLength / duration);
|
||||||
|
this.content_length = contentLength;
|
||||||
|
this.request = null;
|
||||||
|
this.timer = new Timer(() => {
|
||||||
|
this.timer.reuse();
|
||||||
|
this.loop();
|
||||||
|
}, 265);
|
||||||
|
this.stream.on('close', () => {
|
||||||
|
this.timer.destroy();
|
||||||
|
this.cleanup();
|
||||||
|
});
|
||||||
|
this.seek(options.seek!)
|
||||||
|
}
|
||||||
|
|
||||||
|
private seek(ms : number){
|
||||||
|
return new Promise(async(res) => {
|
||||||
|
const stream = await request_stream(this.url, {
|
||||||
|
headers: {
|
||||||
|
range: `bytes=0-1000`
|
||||||
|
}
|
||||||
|
}).catch((err: Error) => err);
|
||||||
|
|
||||||
|
if (stream instanceof Error) {
|
||||||
|
this.stream.emit('error', stream);
|
||||||
|
this.bytes_count = 0;
|
||||||
|
this.per_sec_bytes = 0;
|
||||||
|
this.cleanup();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.request = stream
|
||||||
|
stream.pipe(this.stream, { end : false })
|
||||||
|
|
||||||
|
stream.once('end', () => {
|
||||||
|
this.stream.state = WebmSeekerState.READING_DATA
|
||||||
|
|
||||||
|
const bytes = this.stream.seek(ms)
|
||||||
|
if (bytes instanceof Error) {
|
||||||
|
this.stream.emit('error', bytes);
|
||||||
|
this.bytes_count = 0;
|
||||||
|
this.per_sec_bytes = 0;
|
||||||
|
this.cleanup();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bytes_count = bytes
|
||||||
|
this.timer.reuse()
|
||||||
|
this.loop()
|
||||||
|
res('')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Retry if we get 404 or 403 Errors.
|
||||||
|
*/
|
||||||
|
private async retry() {
|
||||||
|
const info = await video_info(this.video_url);
|
||||||
|
const audioFormat = parseAudioFormats(info.format);
|
||||||
|
this.url = audioFormat[this.quality].url;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* This cleans every used variable in class.
|
||||||
|
*
|
||||||
|
* This is used to prevent re-use of this class and helping garbage collector to collect it.
|
||||||
|
*/
|
||||||
|
private cleanup() {
|
||||||
|
this.request?.destroy();
|
||||||
|
this.request = null;
|
||||||
|
this.url = '';
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Getting data from audio endpoint url and passing it to stream.
|
||||||
|
*
|
||||||
|
* If 404 or 403 occurs, it will retry again.
|
||||||
|
*/
|
||||||
|
private async loop() {
|
||||||
|
if (this.stream.destroyed) {
|
||||||
|
this.timer.destroy();
|
||||||
|
this.cleanup();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const end: number = this.bytes_count + this.per_sec_bytes * 300;
|
||||||
|
const stream = await request_stream(this.url, {
|
||||||
|
headers: {
|
||||||
|
range: `bytes=${this.bytes_count}-${end >= this.content_length ? '' : end}`
|
||||||
|
}
|
||||||
|
}).catch((err: Error) => err);
|
||||||
|
if (stream instanceof Error) {
|
||||||
|
this.stream.emit('error', stream);
|
||||||
|
this.bytes_count = 0;
|
||||||
|
this.per_sec_bytes = 0;
|
||||||
|
this.cleanup();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Number(stream.statusCode) >= 400) {
|
||||||
|
this.cleanup();
|
||||||
|
await this.retry();
|
||||||
|
this.timer.reuse();
|
||||||
|
this.loop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.request = stream;
|
||||||
|
stream.pipe(this.stream)
|
||||||
|
|
||||||
|
stream.once('error', async () => {
|
||||||
|
this.cleanup();
|
||||||
|
await this.retry();
|
||||||
|
this.timer.reuse();
|
||||||
|
this.loop();
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on('data', (chunk: any) => {
|
||||||
|
this.bytes_count += chunk.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on('end', () => {
|
||||||
|
if (end >= this.content_length) {
|
||||||
|
this.timer.destroy();
|
||||||
|
this.stream.push(null);
|
||||||
|
this.cleanup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Pauses timer.
|
||||||
|
* Stops running of loop.
|
||||||
|
*
|
||||||
|
* Useful if you don't want to get excess data to be stored in stream.
|
||||||
|
*/
|
||||||
|
pause() {
|
||||||
|
this.timer.pause();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Resumes timer.
|
||||||
|
* Starts running of loop.
|
||||||
|
*/
|
||||||
|
resume() {
|
||||||
|
this.timer.resume();
|
||||||
|
}
|
||||||
|
}
|
||||||
207
play-dl/YouTube/classes/WebmSeeker.ts
Normal file
207
play-dl/YouTube/classes/WebmSeeker.ts
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
import { WebmElements, WebmHeader } from 'play-audio'
|
||||||
|
import { Duplex, DuplexOptions } from 'stream'
|
||||||
|
|
||||||
|
enum DataType { master, string, uint, binary, float }
|
||||||
|
|
||||||
|
export enum WebmSeekerState{
|
||||||
|
READING_HEAD = 'READING_HEAD',
|
||||||
|
READING_DATA = 'READING_DATA',
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WebmSeeker extends Duplex{
|
||||||
|
remaining? : Buffer
|
||||||
|
state : WebmSeekerState
|
||||||
|
chunk? : Buffer
|
||||||
|
cursor : number
|
||||||
|
header : WebmHeader
|
||||||
|
headfound : boolean
|
||||||
|
seekfound : boolean
|
||||||
|
private data_size : number
|
||||||
|
private data_length : number
|
||||||
|
|
||||||
|
constructor(options? : DuplexOptions){
|
||||||
|
super(options)
|
||||||
|
this.state = WebmSeekerState.READING_HEAD
|
||||||
|
this.cursor = 0
|
||||||
|
this.header = new WebmHeader()
|
||||||
|
this.headfound = false
|
||||||
|
this.seekfound = false
|
||||||
|
this.data_length = 0
|
||||||
|
this.data_size = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private get vint_length(): number{
|
||||||
|
let i = 0;
|
||||||
|
for (; i < 8; i++){
|
||||||
|
if ((1 << (7 - i)) & this.chunk![this.cursor])
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return ++i;
|
||||||
|
}
|
||||||
|
|
||||||
|
private get vint_value(): boolean {
|
||||||
|
if (!this.chunk) return false
|
||||||
|
const length = this.vint_length
|
||||||
|
if(this.chunk.length < this.cursor + length) return false
|
||||||
|
let value = this.chunk[this.cursor] & ((1 << (8 - length)) - 1)
|
||||||
|
for (let i = this.cursor + 1; i < this.cursor + length; i++) value = (value << 8) + this.chunk[i];
|
||||||
|
this.data_size = length
|
||||||
|
this.data_length = value
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup(){
|
||||||
|
this.cursor = 0
|
||||||
|
this.chunk = undefined
|
||||||
|
this.remaining = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
_read() {}
|
||||||
|
|
||||||
|
seek(ms : number): Error | number{
|
||||||
|
let position = 0
|
||||||
|
let time = (Math.floor(ms / 10) * 10)
|
||||||
|
if (!this.header.segment.cues) return new Error("Failed to Parse Cues")
|
||||||
|
|
||||||
|
for(const data of this.header.segment.cues){
|
||||||
|
if(Math.floor(data.time as number / 1000) === time) {
|
||||||
|
position = data.position as number
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else continue;
|
||||||
|
}
|
||||||
|
if(position === 0) return Error("Failed to find Cluster Position")
|
||||||
|
else return position
|
||||||
|
}
|
||||||
|
|
||||||
|
_write(chunk: Buffer, _: BufferEncoding, callback: (error?: Error | null) => void): void {
|
||||||
|
if (this.remaining) {
|
||||||
|
this.chunk = Buffer.concat([this.remaining, chunk])
|
||||||
|
this.remaining = undefined
|
||||||
|
}
|
||||||
|
else this.chunk = chunk
|
||||||
|
|
||||||
|
let err : Error | undefined;
|
||||||
|
|
||||||
|
if(this.state === WebmSeekerState.READING_HEAD) err = this.readHead()
|
||||||
|
else if(!this.seekfound) err = this.getClosetCluster()
|
||||||
|
else err = this.readTag()
|
||||||
|
|
||||||
|
if(err) callback(err)
|
||||||
|
else callback()
|
||||||
|
}
|
||||||
|
|
||||||
|
private readHead(): Error | undefined{
|
||||||
|
if (!this.chunk) return new Error("Chunk is missing")
|
||||||
|
|
||||||
|
while(this.chunk.length > this.cursor ){
|
||||||
|
const oldCursor = this.cursor
|
||||||
|
const id = this.vint_length
|
||||||
|
if(this.chunk.length < this.cursor + id) break;
|
||||||
|
|
||||||
|
const ebmlID = this.parseEbmlID(this.chunk.slice(this.cursor, this.cursor + id).toString('hex'))
|
||||||
|
this.cursor += id
|
||||||
|
const vint = this.vint_value
|
||||||
|
|
||||||
|
if(!vint) {
|
||||||
|
this.cursor = oldCursor
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(!ebmlID){
|
||||||
|
this.cursor += this.data_size + this.data_length
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!this.headfound){
|
||||||
|
if(ebmlID.name === "ebml") this.headfound = true
|
||||||
|
else return new Error("Failed to find EBML ID at start of stream.")
|
||||||
|
}
|
||||||
|
const data = this.chunk.slice(this.cursor + this.data_size, this.cursor + this.data_size + this.data_length)
|
||||||
|
const parse = this.header.parse(ebmlID, data)
|
||||||
|
if(parse instanceof Error) return parse
|
||||||
|
|
||||||
|
if(ebmlID.type === DataType.master) {
|
||||||
|
this.cursor += this.data_size
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.chunk.length < this.cursor + this.data_size + this.data_length) {
|
||||||
|
this.cursor = oldCursor;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else this.cursor += this.data_size + this.data_length
|
||||||
|
}
|
||||||
|
this.remaining = this.chunk.slice(this.cursor)
|
||||||
|
this.cursor = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private readTag(): Error | undefined{
|
||||||
|
if (!this.chunk) return new Error("Chunk is missing")
|
||||||
|
|
||||||
|
while(this.chunk.length > this.cursor ){
|
||||||
|
const oldCursor = this.cursor
|
||||||
|
const id = this.vint_length
|
||||||
|
if(this.chunk.length < this.cursor + id) break;
|
||||||
|
|
||||||
|
const ebmlID = this.parseEbmlID(this.chunk.slice(this.cursor, this.cursor + id).toString('hex'))
|
||||||
|
this.cursor += id
|
||||||
|
const vint = this.vint_value
|
||||||
|
|
||||||
|
if(!vint) {
|
||||||
|
this.cursor = oldCursor
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if(!ebmlID){
|
||||||
|
this.cursor += this.data_size + this.data_length
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = this.chunk.slice(this.cursor + this.data_size, this.cursor + this.data_size + this.data_length)
|
||||||
|
const parse = this.header.parse(ebmlID, data)
|
||||||
|
if(parse instanceof Error) return parse
|
||||||
|
|
||||||
|
if(ebmlID.type === DataType.master) {
|
||||||
|
this.cursor += this.data_size
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.chunk.length < this.cursor + this.data_size + this.data_length) {
|
||||||
|
this.cursor = oldCursor;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else this.cursor += this.data_size + this.data_length
|
||||||
|
|
||||||
|
if(ebmlID.name === 'simpleBlock'){
|
||||||
|
const track = this.header.segment.tracks![this.header.audioTrack]
|
||||||
|
if(!track || track.trackType !== 2) return new Error("No audio Track in this webm file.")
|
||||||
|
if((data[0] & 0xf) === track.trackNumber) this.push(data.slice(4))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.remaining = this.chunk.slice(this.cursor)
|
||||||
|
this.cursor = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private getClosetCluster(): Error | undefined{
|
||||||
|
if(!this.chunk) return new Error("Chunk is missing")
|
||||||
|
const count = this.chunk.indexOf('1f43b675', 0, 'hex')
|
||||||
|
if(count === -1) throw new Error("Failed to find nearest Cluster.")
|
||||||
|
else this.chunk = this.chunk.slice(count)
|
||||||
|
this.seekfound = true
|
||||||
|
return this.readTag()
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseEbmlID(ebmlID : string){
|
||||||
|
if(Object.keys(WebmElements).includes(ebmlID)) return WebmElements[ebmlID]
|
||||||
|
else return false
|
||||||
|
}
|
||||||
|
|
||||||
|
_destroy(error : Error | null, callback : (error : Error | null) => void) : void {
|
||||||
|
this.cleanup()
|
||||||
|
callback(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
_final(callback: (error?: Error | null) => void): void {
|
||||||
|
this.cleanup();
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import { LiveStream, Stream } from './classes/LiveStream';
|
import { LiveStream, Stream } from './classes/LiveStream';
|
||||||
|
import { SeekStream } from './classes/SeekStream';
|
||||||
import { InfoData, StreamInfoData } from './utils/constants';
|
import { InfoData, StreamInfoData } from './utils/constants';
|
||||||
import { video_stream_info } from './utils/extractor';
|
import { video_stream_info } from './utils/extractor';
|
||||||
|
|
||||||
@ -11,6 +12,7 @@ export enum StreamType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface StreamOptions {
|
export interface StreamOptions {
|
||||||
|
seek? : number
|
||||||
quality?: number;
|
quality?: number;
|
||||||
htmldata?: boolean;
|
htmldata?: boolean;
|
||||||
}
|
}
|
||||||
@ -35,7 +37,7 @@ export function parseAudioFormats(formats: any[]) {
|
|||||||
/**
|
/**
|
||||||
* Type for YouTube Stream
|
* Type for YouTube Stream
|
||||||
*/
|
*/
|
||||||
export type YouTubeStream = Stream | LiveStream;
|
export type YouTubeStream = Stream | LiveStream | SeekStream;
|
||||||
/**
|
/**
|
||||||
* Stream command for YouTube
|
* Stream command for YouTube
|
||||||
* @param url YouTube URL
|
* @param url YouTube URL
|
||||||
@ -77,7 +79,19 @@ export async function stream_from_info(
|
|||||||
else final.push(info.format[info.format.length - 1]);
|
else final.push(info.format[info.format.length - 1]);
|
||||||
let type: StreamType =
|
let type: StreamType =
|
||||||
final[0].codec === 'opus' && final[0].container === 'webm' ? StreamType.WebmOpus : StreamType.Arbitrary;
|
final[0].codec === 'opus' && final[0].container === 'webm' ? StreamType.WebmOpus : StreamType.Arbitrary;
|
||||||
return new Stream(
|
if(options.seek){
|
||||||
|
if(type === StreamType.WebmOpus) {
|
||||||
|
return new SeekStream(
|
||||||
|
final[0].url,
|
||||||
|
info.video_details.durationInSec,
|
||||||
|
Number(final[0].contentLength),
|
||||||
|
info.video_details.url,
|
||||||
|
options
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else throw new Error("Seek is only supported in Webm Opus Files.")
|
||||||
|
}
|
||||||
|
else return new Stream(
|
||||||
final[0].url,
|
final[0].url,
|
||||||
type,
|
type,
|
||||||
info.video_details.durationInSec,
|
info.video_details.durationInSec,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user