@@ -239,13 +239,13 @@ class FormatUtils {
239239 return candidates [ 0 ] ;
240240 }
241241
242- static toDash ( streaming_data ?: {
242+ static async toDash ( streaming_data ?: {
243243 expires : Date ;
244244 formats : Format [ ] ;
245245 adaptive_formats : Format [ ] ;
246246 dash_manifest_url : string | null ;
247247 hls_manifest_url : string | null ;
248- } , url_transformer : URLTransformer = ( url ) => url , format_filter ?: FormatFilter , cpn ?: string , player ?: Player ) : string {
248+ } , url_transformer : URLTransformer = ( url ) => url , format_filter ?: FormatFilter , cpn ?: string , player ?: Player , actions ?: Actions ) : Promise < string > {
249249 if ( ! streaming_data )
250250 throw new InnertubeError ( 'Streaming data not available' ) ;
251251
@@ -288,7 +288,7 @@ class FormatUtils {
288288 period
289289 ] ) ) ;
290290
291- this . #generateAdaptationSet( document , period , adaptive_formats , url_transformer , cpn , player ) ;
291+ await this . #generateAdaptationSet( document , period , adaptive_formats , url_transformer , cpn , player , actions ) ;
292292
293293 return Platform . shim . serializeDOM ( document ) ;
294294 }
@@ -305,12 +305,12 @@ class FormatUtils {
305305 return el ;
306306 }
307307
308- static #generateAdaptationSet( document : XMLDocument , period : Element , formats : Format [ ] , url_transformer : URLTransformer , cpn ?: string , player ?: Player ) {
308+ static async #generateAdaptationSet( document : XMLDocument , period : Element , formats : Format [ ] , url_transformer : URLTransformer , cpn ?: string , player ?: Player , actions ?: Actions ) {
309309 const mime_types : string [ ] = [ ] ;
310310 const mime_objects : Format [ ] [ ] = [ [ ] ] ;
311311
312312 formats . forEach ( ( video_format ) => {
313- if ( ! video_format . index_range || ! video_format . init_range ) {
313+ if ( ( ! video_format . index_range || ! video_format . init_range ) && ! video_format . is_type_otf ) {
314314 return ;
315315 }
316316 const mime_type = video_format . mime_type ;
@@ -376,9 +376,9 @@ class FormatUtils {
376376
377377 period . appendChild ( set ) ;
378378
379- track_objects [ j ] . forEach ( ( format ) => {
380- this . #generateRepresentationAudio( document , set , format , url_transformer , cpn , player ) ;
381- } ) ;
379+ for ( const format of track_objects [ j ] ) {
380+ await this . #generateRepresentationAudio( document , set , format , url_transformer , cpn , player , actions ) ;
381+ }
382382 }
383383 } else {
384384 const set = this . #el( document , 'AdaptationSet' , {
@@ -390,57 +390,45 @@ class FormatUtils {
390390
391391 period . appendChild ( set ) ;
392392
393- mime_objects [ i ] . forEach ( ( format ) => {
393+ for ( const format of mime_objects [ i ] ) {
394394 if ( format . has_video ) {
395- this . #generateRepresentationVideo( document , set , format , url_transformer , cpn , player ) ;
395+ await this . #generateRepresentationVideo( document , set , format , url_transformer , cpn , player , actions ) ;
396396 } else {
397- this . #generateRepresentationAudio( document , set , format , url_transformer , cpn , player ) ;
397+ await this . #generateRepresentationAudio( document , set , format , url_transformer , cpn , player , actions ) ;
398398 }
399- } ) ;
399+ }
400400 }
401401 }
402402 }
403403
404- static #generateRepresentationVideo( document : XMLDocument , set : Element , format : Format , url_transformer : URLTransformer , cpn ?: string , player ?: Player ) {
404+ static async #generateRepresentationVideo( document : XMLDocument , set : Element , format : Format , url_transformer : URLTransformer , cpn ?: string , player ?: Player , actions ?: Actions ) {
405405 const codecs = getStringBetweenStrings ( format . mime_type , 'codecs="' , '"' ) ;
406406
407- if ( ! format . index_range || ! format . init_range )
408- throw new InnertubeError ( 'Index and init ranges not available' , { format } ) ;
409-
410407 const url = new URL ( format . decipher ( player ) ) ;
411408 url . searchParams . set ( 'cpn' , cpn || '' ) ;
412409
413- set . appendChild ( this . #el( document , 'Representation' , {
410+ const representation = this . #el( document , 'Representation' , {
414411 id : format . itag ?. toString ( ) ,
415412 codecs,
416413 bandwidth : format . bitrate ?. toString ( ) ,
417414 width : format . width ?. toString ( ) ,
418415 height : format . height ?. toString ( ) ,
419416 maxPlayoutRate : '1' ,
420417 frameRate : format . fps ?. toString ( )
421- } , [
422- this . #el( document , 'BaseURL' , { } , [
423- document . createTextNode ( url_transformer ( url ) ?. toString ( ) )
424- ] ) ,
425- this . #el( document , 'SegmentBase' , {
426- indexRange : `${ format . index_range . start } -${ format . index_range . end } `
427- } , [
428- this . #el( document , 'Initialization' , {
429- range : `${ format . init_range . start } -${ format . init_range . end } `
430- } )
431- ] )
432- ] ) ) ;
418+ } ) ;
419+
420+ set . appendChild ( representation ) ;
421+
422+ await this . #generateSegmentInformation( document , representation , format , url_transformer ( url ) ?. toString ( ) , actions ) ;
433423 }
434424
435- static async #generateRepresentationAudio( document : XMLDocument , set : Element , format : Format , url_transformer : URLTransformer , cpn ?: string , player ?: Player ) {
425+ static async #generateRepresentationAudio( document : XMLDocument , set : Element , format : Format , url_transformer : URLTransformer , cpn ?: string , player ?: Player , actions ?: Actions ) {
436426 const codecs = getStringBetweenStrings ( format . mime_type , 'codecs="' , '"' ) ;
437- if ( ! format . index_range || ! format . init_range )
438- throw new InnertubeError ( 'Index and init ranges not available' , { format } ) ;
439427
440428 const url = new URL ( format . decipher ( player ) ) ;
441429 url . searchParams . set ( 'cpn' , cpn || '' ) ;
442430
443- set . appendChild ( this . #el( document , 'Representation' , {
431+ const representation = this . #el( document , 'Representation' , {
444432 id : format . itag ?. toString ( ) ,
445433 codecs,
446434 bandwidth : format . bitrate ?. toString ( ) ,
@@ -449,18 +437,127 @@ class FormatUtils {
449437 this . #el( document , 'AudioChannelConfiguration' , {
450438 schemeIdUri : 'urn:mpeg:dash:23003:3:audio_channel_configuration:2011' ,
451439 value : format . audio_channels ?. toString ( ) || '2'
452- } ) ,
453- this . #el( document , 'BaseURL' , { } , [
454- document . createTextNode ( url_transformer ( url ) ?. toString ( ) )
455- ] ) ,
456- this . #el( document , 'SegmentBase' , {
457- indexRange : `${ format . index_range . start } -${ format . index_range . end } `
458- } , [
459- this . #el( document , 'Initialization' , {
460- range : `${ format . init_range . start } -${ format . init_range . end } `
461- } )
462- ] )
463- ] ) ) ;
440+ } )
441+ ] ) ;
442+
443+ set . appendChild ( representation ) ;
444+
445+ await this . #generateSegmentInformation( document , representation , format , url_transformer ( url ) ?. toString ( ) , actions ) ;
446+ }
447+
448+ static async #generateSegmentInformation( document : XMLDocument , representation : Element , format : Format , url : string , actions ?: Actions ) {
449+ if ( format . is_type_otf ) {
450+ if ( ! actions ) {
451+ throw new InnertubeError ( 'Unable to get segment durations for this OTF stream without an Actions instance' , { format } ) ;
452+ }
453+
454+ const { resolved_url, segment_durations } = await this . #getOTFSegmentInformation( url , actions ) ;
455+ const segment_elements = [ ] ;
456+
457+ for ( const segment_duration of segment_durations ) {
458+ let attributes ;
459+
460+ if ( typeof segment_duration . repeat_count === 'undefined' ) {
461+ attributes = {
462+ d : segment_duration . duration . toString ( )
463+ } ;
464+ } else {
465+ attributes = {
466+ d : segment_duration . duration . toString ( ) ,
467+ r : segment_duration . repeat_count . toString ( )
468+ } ;
469+ }
470+ segment_elements . push ( this . #el( document , 'S' , attributes ) ) ;
471+ }
472+
473+ representation . appendChild (
474+ this . #el( document , 'SegmentTemplate' , {
475+ startNumber : '1' ,
476+ timescale : '1000' ,
477+ initialization : `${ resolved_url } &sq=0` ,
478+ media : `${ resolved_url } &sq=$Number$`
479+ } , [
480+ this . #el( document , 'SegmentTimeline' , { } , segment_elements )
481+ ] )
482+ ) ;
483+ } else {
484+ if ( ! format . index_range || ! format . init_range )
485+ throw new InnertubeError ( 'Index and init ranges not available' , { format } ) ;
486+
487+ representation . appendChild (
488+ this . #el( document , 'BaseURL' , { } , [
489+ document . createTextNode ( url )
490+ ] )
491+ ) ;
492+ representation . appendChild (
493+ this . #el( document , 'SegmentBase' , {
494+ indexRange : `${ format . index_range . start } -${ format . index_range . end } `
495+ } , [
496+ this . #el( document , 'Initialization' , {
497+ range : `${ format . init_range . start } -${ format . init_range . end } `
498+ } )
499+ ] )
500+ ) ;
501+ }
502+ }
503+
504+ static async #getOTFSegmentInformation( url : string , actions : Actions ) : Promise < {
505+ resolved_url : string ,
506+ segment_durations : {
507+ duration : number ,
508+ repeat_count ?: number
509+ } [ ]
510+ } > {
511+ // Fetch the first segment as it contains the segment durations which we need to generate the manifest
512+ const response = await actions . session . http . fetch_function ( `${ url } &rn=0&sq=0` , {
513+ method : 'GET' ,
514+ headers : Constants . STREAM_HEADERS ,
515+ redirect : 'follow'
516+ } ) ;
517+
518+ // Example OTF video: https://www.youtube.com/watch?v=DJ8GQUNUXGM
519+
520+ // There might have been redirects, if there were we want to write the resolved URL to the manifest
521+ // So that the player doesn't have to follow the redirects every time it requests a segment
522+ const resolved_url = response . url . replace ( '&rn=0' , '' ) . replace ( '&sq=0' , '' ) ;
523+
524+ // In this function we only need the segment durations and how often the durations are repeated
525+ // The segment count could be useful for other stuff though
526+ // The response body contains a lot of junk but the useful stuff looks like this:
527+ // Segment-Count: 922\r\n' +
528+ // 'Segment-Durations-Ms: 5120(r=920),3600,\r\n'
529+ const response_text = await response . text ( ) ;
530+
531+ const segment_duration_strings = getStringBetweenStrings ( response_text , 'Segment-Durations-Ms:' , '\r\n' ) ?. split ( ',' ) ;
532+
533+ if ( ! segment_duration_strings ) {
534+ throw new InnertubeError ( 'Failed to extract the segment durations from this OTF stream' , { url } ) ;
535+ }
536+
537+ const segment_durations = [ ] ;
538+ for ( const segment_duration_string of segment_duration_strings ) {
539+ const trimmed_segment_duration = segment_duration_string . trim ( ) ;
540+ if ( trimmed_segment_duration . length === 0 ) {
541+ continue ;
542+ }
543+
544+ let repeat_count ;
545+
546+ const repeat_count_string = getStringBetweenStrings ( trimmed_segment_duration , '(r=' , ')' ) ;
547+ if ( repeat_count_string ) {
548+ repeat_count = parseInt ( repeat_count_string ) ;
549+ }
550+
551+ segment_durations . push ( {
552+ duration : parseInt ( trimmed_segment_duration ) ,
553+ repeat_count
554+ } ) ;
555+ }
556+
557+ return {
558+ resolved_url,
559+ segment_durations
560+ } ;
464561 }
465562}
466563
0 commit comments