@@ -11,6 +11,7 @@ var subtitleTypes = require('./subtitleTypes');
1111var createASSRenderer = require ( './assRenderer' ) ;
1212
1313const PREVIEW_INTERVAL = 300000 ;
14+ const NATIVE_ASS_TIMEOUT = 10000 ;
1415
1516function withHTMLSubtitles ( Video ) {
1617 function VideoWithHTMLSubtitles ( options ) {
@@ -140,6 +141,13 @@ function withHTMLSubtitles(Video) {
140141 var usingASSRenderer = false ;
141142 var selectedSubtitleText = null ;
142143 var selectionRequestId = 0 ;
144+ var nativeAssSubtitlesSupported = options . shellTransport &&
145+ options . shellTransport . capabilities &&
146+ options . shellTransport . capabilities . nativeAssSubtitles === true ;
147+ var nativeAssTrackId = null ;
148+ var pendingNativeAss = null ;
149+ var abandonedNativeAssTitles = [ ] ;
150+ var removingNativeAssTrackIds = [ ] ;
143151
144152 var observedProps = {
145153 extraSubtitlesTracks : false ,
@@ -268,6 +276,10 @@ function withHTMLSubtitles(Video) {
268276 videoAssSubtitlesStylingActive = propValue === true ;
269277 break ;
270278 }
279+ case 'subtitlesTracks' : {
280+ updateNativeASSTrack ( propValue ) ;
281+ break ;
282+ }
271283 case 'paused' : {
272284 if ( propValue && ! videoState . paused && ! videoState . buffering && videoState . lastSyncAt !== null && videoState . time !== null ) {
273285 videoState . time = videoState . time + ( Date . now ( ) - videoState . lastSyncAt ) * videoState . playbackSpeed ;
@@ -302,7 +314,7 @@ function withHTMLSubtitles(Video) {
302314
303315 events . emit ( eventName , propName , getProp ( propName , propValue ) ) ;
304316
305- if ( propName === 'selectedSubtitlesTrackId' && propValue !== null && selectedTrackId !== null && nativeTextTrack === null ) {
317+ if ( propName === 'selectedSubtitlesTrackId' && propValue !== null && selectedTrackId !== null && nativeTextTrack === null && ! isNativeASSSelection ( propValue ) ) {
306318 setProp ( 'selectedExtraSubtitlesTrackId' , null ) ;
307319 }
308320 }
@@ -330,6 +342,95 @@ function withHTMLSubtitles(Video) {
330342 onPropChanged ( 'assSubtitlesStylingActive' ) ;
331343 }
332344 }
345+ function getNativeASSRawTrackId ( trackId ) {
346+ return typeof trackId === 'string' && trackId . startsWith ( 'EMBEDDED_' ) ?
347+ trackId . slice ( 'EMBEDDED_' . length ) :
348+ null ;
349+ }
350+ function removeNativeASSTrack ( trackId ) {
351+ var rawTrackId = getNativeASSRawTrackId ( trackId ) ;
352+ if ( rawTrackId !== null ) {
353+ options . shellTransport . send ( 'mpv-command' , [ 'sub-remove' , rawTrackId ] ) ;
354+ }
355+ }
356+ function clearNativeASS ( ) {
357+ if ( pendingNativeAss !== null ) {
358+ clearTimeout ( pendingNativeAss . timeout ) ;
359+ if ( ! abandonedNativeAssTitles . includes ( pendingNativeAss . title ) ) {
360+ abandonedNativeAssTitles . push ( pendingNativeAss . title ) ;
361+ }
362+ pendingNativeAss = null ;
363+ }
364+ if ( nativeAssTrackId !== null ) {
365+ removeNativeASSTrack ( nativeAssTrackId ) ;
366+ nativeAssTrackId = null ;
367+ }
368+ }
369+ function isNativeASSSelection ( trackId ) {
370+ return nativeAssTrackId === trackId || pendingNativeAss !== null ;
371+ }
372+ function getPublicSubtitleTrack ( track ) {
373+ var publicTrack = Object . assign ( { } , track ) ;
374+ delete publicTrack . nativeExternal ;
375+ delete publicTrack . nativeExternalFilename ;
376+ delete publicTrack . nativeExternalTitle ;
377+ return publicTrack ;
378+ }
379+ function isManagedNativeASSTrack ( track ) {
380+ return track . id === nativeAssTrackId ||
381+ removingNativeAssTrackIds . includes ( track . id ) ||
382+ ( pendingNativeAss !== null && track . nativeExternalTitle === pendingNativeAss . title ) ||
383+ abandonedNativeAssTitles . includes ( track . nativeExternalTitle ) ;
384+ }
385+ function updateNativeASSTrack ( subtitleTracks ) {
386+ if ( ! Array . isArray ( subtitleTracks ) ) {
387+ return ;
388+ }
389+
390+ removingNativeAssTrackIds = removingNativeAssTrackIds . filter ( function ( trackId ) {
391+ return subtitleTracks . some ( function ( track ) { return track . id === trackId ; } ) ;
392+ } ) ;
393+ abandonedNativeAssTitles = abandonedNativeAssTitles . filter ( function ( title ) {
394+ var abandonedTracks = subtitleTracks . filter ( function ( track ) {
395+ return track . nativeExternalTitle === title ;
396+ } ) ;
397+ abandonedTracks . forEach ( function ( track ) {
398+ removingNativeAssTrackIds . push ( track . id ) ;
399+ removeNativeASSTrack ( track . id ) ;
400+ } ) ;
401+ return abandonedTracks . length === 0 ;
402+ } ) ;
403+
404+ if ( pendingNativeAss === null ) {
405+ return ;
406+ }
407+ var matchingTrack = subtitleTracks . slice ( ) . reverse ( ) . find ( function ( track ) {
408+ return track . nativeExternalTitle === pendingNativeAss . title ;
409+ } ) ;
410+ if ( ! matchingTrack ) {
411+ return ;
412+ }
413+
414+ var loadedTrack = pendingNativeAss . track ;
415+ clearTimeout ( pendingNativeAss . timeout ) ;
416+ pendingNativeAss = null ;
417+ nativeAssTrackId = matchingTrack . id ;
418+ video . dispatch ( {
419+ type : 'setProp' ,
420+ propName : 'subtitlesDelay' ,
421+ propValue : ( delay || 0 ) / 1000 ,
422+ } ) ;
423+ events . emit ( 'extraSubtitlesTrackLoaded' , loadedTrack ) ;
424+ }
425+ function updateTrackASS ( track , isASS ) {
426+ var trackIndex = tracks . findIndex ( function ( candidate ) {
427+ return candidate . id === track . id ;
428+ } ) ;
429+ if ( trackIndex !== - 1 && tracks [ trackIndex ] . ass !== isASS ) {
430+ tracks [ trackIndex ] = Object . assign ( { } , tracks [ trackIndex ] , { ass : isASS } ) ;
431+ onPropChanged ( 'extraSubtitlesTracks' ) ;
432+ }
433+ }
333434 function reportSubtitleError ( error , track ) {
334435 onError ( Object . assign ( { } , ERROR . WITH_HTML_SUBTITLES . LOAD_FAILED , {
335436 error : error ,
@@ -429,7 +530,43 @@ function withHTMLSubtitles(Video) {
429530 return fallbackFromASS ( error , track , text , currentRequestId ) ;
430531 } ) ;
431532 }
432- function loadSelectedTrack ( track , isFallback , currentRequestId ) {
533+ function loadWebSubtitles ( track , text , isASS , currentRequestId ) {
534+ return isASS && assSubtitlesStylingEnabled ?
535+ loadASSSubtitles ( track , text , currentRequestId ) :
536+ loadPlainSubtitles ( track , text , isASS , currentRequestId ) ;
537+ }
538+ function delegateNativeASS ( track , url , text , currentRequestId ) {
539+ var title = '__stremio_external_ass_' + currentRequestId ;
540+ var pending = {
541+ track : track ,
542+ title : title ,
543+ timeout : null ,
544+ } ;
545+ pending . timeout = setTimeout ( function ( ) {
546+ if ( pendingNativeAss !== pending ||
547+ currentRequestId !== selectionRequestId ||
548+ selectedTrackId !== track . id ) {
549+ return ;
550+ }
551+
552+ pendingNativeAss = null ;
553+ abandonedNativeAssTitles . push ( title ) ;
554+ if ( typeof text === 'string' ) {
555+ loadWebSubtitles ( track , text , true , currentRequestId ) ;
556+ } else {
557+ loadSelectedTrack ( track , false , currentRequestId , true ) ;
558+ }
559+ } , NATIVE_ASS_TIMEOUT ) ;
560+ pendingNativeAss = pending ;
561+ options . shellTransport . send ( 'mpv-command' , [
562+ 'sub-add' ,
563+ url ,
564+ 'select' ,
565+ title ,
566+ track . lang ,
567+ ] ) ;
568+ }
569+ function loadSelectedTrack ( track , isFallback , currentRequestId , forceWeb ) {
433570 return getSubtitlesData ( track , isFallback )
434571 . then ( function ( text ) {
435572 if ( currentRequestId !== selectionRequestId || selectedTrackId !== track . id ) {
@@ -442,16 +579,19 @@ function withHTMLSubtitles(Video) {
442579 fallbackUrl : null
443580 } ) ;
444581 var isASS = subtitleTypes . isASSSubtitle ( sourceTrack , text ) ;
445- return isASS && assSubtitlesStylingEnabled ?
446- loadASSSubtitles ( track , text , currentRequestId ) :
447- loadPlainSubtitles ( track , text , isASS , currentRequestId ) ;
582+ updateTrackASS ( track , isASS ) ;
583+ if ( ! forceWeb && nativeAssSubtitlesSupported && isASS && typeof sourceTrack . url === 'string' ) {
584+ delegateNativeASS ( track , sourceTrack . url , text , currentRequestId ) ;
585+ return null ;
586+ }
587+ return loadWebSubtitles ( track , text , isASS , currentRequestId ) ;
448588 } )
449589 . catch ( function ( error ) {
450590 if ( currentRequestId !== selectionRequestId || selectedTrackId !== track . id ) {
451591 return null ;
452592 }
453593 if ( ! isFallback && typeof track . fallbackUrl === 'string' ) {
454- return loadSelectedTrack ( track , true , currentRequestId ) ;
594+ return loadSelectedTrack ( track , true , currentRequestId , forceWeb ) ;
455595 }
456596
457597 reportSubtitleError ( error , track ) ;
@@ -536,16 +676,25 @@ function withHTMLSubtitles(Video) {
536676 return assSubtitlesStylingActive || videoAssSubtitlesStylingActive ;
537677 }
538678 case 'subtitlesTracks' : {
539- if ( Array . isArray ( videoPropValue ) && videoElement && videoElement . textTracks ) {
679+ if ( Array . isArray ( videoPropValue ) ) {
540680 return videoPropValue . filter ( function ( track ) {
681+ if ( isManagedNativeASSTrack ( track ) ) {
682+ return false ;
683+ }
684+ if ( ! videoElement || ! videoElement . textTracks ) {
685+ return true ;
686+ }
541687 var index = getEmbeddedTrackIndex ( track . id ) ;
542688 return index === null || ! isNativeTextTrack ( videoElement . textTracks [ index ] ) ;
543- } ) ;
689+ } ) . map ( getPublicSubtitleTrack ) ;
544690 }
545691
546692 return videoPropValue ;
547693 }
548694 case 'selectedSubtitlesTrackId' : {
695+ if ( isNativeASSSelection ( videoPropValue ) ) {
696+ return null ;
697+ }
549698 if ( typeof videoPropValue === 'string' && videoElement && videoElement . textTracks ) {
550699 var index = getEmbeddedTrackIndex ( videoPropValue ) ;
551700 return index !== null && isNativeTextTrack ( videoElement . textTracks [ index ] ) ? null : videoPropValue ;
@@ -591,16 +740,17 @@ function withHTMLSubtitles(Video) {
591740 function setProp ( propName , propValue ) {
592741 switch ( propName ) {
593742 case 'selectedExtraSubtitlesTrackId' : {
743+ if ( propValue !== null && selectedTrackId === propValue ) {
744+ return true ;
745+ }
746+ clearNativeASS ( ) ;
594747 if ( propValue !== null ) {
595748 video . dispatch ( {
596749 type : 'setProp' ,
597750 propName : 'selectedSubtitlesTrackId' ,
598751 propValue : null ,
599752 } ) ;
600753 }
601- if ( propValue !== null && selectedTrackId === propValue ) {
602- return true ;
603- }
604754 var currentRequestId = ++ selectionRequestId ;
605755 cuesByTime = null ;
606756 selectedTrackId = null ;
@@ -621,7 +771,14 @@ function withHTMLSubtitles(Video) {
621771 if ( selectedTrack ) {
622772 selectedTrackId = selectedTrack . id ;
623773 delay = 0 ;
624- loadSelectedTrack ( selectedTrack , false , currentRequestId ) ;
774+ var selectedSourceIsASS = subtitleTypes . isASSSubtitleTrack ( Object . assign ( { } , selectedTrack , {
775+ fallbackUrl : null ,
776+ } ) ) ;
777+ if ( nativeAssSubtitlesSupported && selectedSourceIsASS && typeof selectedTrack . url === 'string' ) {
778+ delegateNativeASS ( selectedTrack , selectedTrack . url , null , currentRequestId ) ;
779+ } else {
780+ loadSelectedTrack ( selectedTrack , false , currentRequestId , false ) ;
781+ }
625782 }
626783 renderSubtitles ( ) ;
627784 onPropChanged ( 'selectedExtraSubtitlesTrackId' ) ;
@@ -632,6 +789,13 @@ function withHTMLSubtitles(Video) {
632789 if ( selectedTrackId !== null && propValue !== null && isFinite ( propValue ) ) {
633790 delay = parseInt ( propValue , 10 ) ;
634791 assRenderer . setDelay ( delay ) ;
792+ if ( nativeAssTrackId !== null || pendingNativeAss !== null ) {
793+ video . dispatch ( {
794+ type : 'setProp' ,
795+ propName : 'subtitlesDelay' ,
796+ propValue : delay / 1000 ,
797+ } ) ;
798+ }
635799 forceRender = true ;
636800 renderSubtitles ( ) ;
637801 onPropChanged ( 'extraSubtitlesDelay' ) ;
@@ -734,7 +898,11 @@ function withHTMLSubtitles(Video) {
734898 case 'addExtraSubtitlesTracks' : {
735899 if ( commandArgs && Array . isArray ( commandArgs . tracks ) ) {
736900 tracks = tracks
737- . concat ( commandArgs . tracks )
901+ . concat ( commandArgs . tracks . map ( function ( track ) {
902+ return Object . assign ( { } , track , {
903+ ass : subtitleTypes . isASSSubtitleTrack ( track ) ,
904+ } ) ;
905+ } ) )
738906 . filter ( function ( track , index , tracks ) {
739907 return track &&
740908 typeof track . id === 'string' &&
@@ -765,6 +933,7 @@ function withHTMLSubtitles(Video) {
765933 origin : 'LOCAL' ,
766934 local : true ,
767935 embedded : false ,
936+ ass : subtitleTypes . isASSSubtitleTrack ( { filename : commandArgs . filename } ) ,
768937 } ;
769938
770939 tracks . push ( track ) ;
@@ -794,6 +963,9 @@ function withHTMLSubtitles(Video) {
794963 }
795964 case 'unload' : {
796965 selectionRequestId = selectionRequestId + 1 ;
966+ clearNativeASS ( ) ;
967+ abandonedNativeAssTitles = [ ] ;
968+ removingNativeAssTrackIds = [ ] ;
797969 removeNativeTrack ( ) ;
798970 stopRenderLoop ( ) ;
799971 lastTimeIndex = null ;
0 commit comments