@@ -113,6 +113,37 @@ export default class TracksStore extends EventEmitter<ITracksStoreEvents> {
113113 text : "error" | "continue" ;
114114 } ;
115115
116+ /**
117+ * Optionally indicate a preferred "codec family" for the next audio and
118+ * video track we encounter. The "codec family" is here just the string
119+ * indicating the codec identifier without the profile/level part if it exists
120+ * (e.g.: mp4a.40.2 => mp4a, avc1.64001e => avc1 etc.).
121+ *
122+ * Reasoning: For future audio / video periods without explicit settings, we
123+ * have to make our own choices for the initial track.
124+ *
125+ * Historically, we went from an order deduced only from the content (the
126+ * Manifest attributes and / or ordering), but there are also legitimate
127+ * reasons to also decide based on codec compatibility: e.g. to allow codec
128+ * continuity (with the possible other Periods we played until now with that
129+ * content).
130+ *
131+ * Doing this allows to reduce the probability of having decoding glitches,
132+ * temporary reloading, weird user experience changes etc.
133+ */
134+ private _preferredCodecFamily : {
135+ /**
136+ * The preferred audio "codec family", e.g. `ec-3`, `mp4a` etc.
137+ * `null` if there's no preference.
138+ */
139+ audio : string | null ;
140+ /**
141+ * The preferred video "codec family", e.g. `avc1`, `hev1`, `vp8` etc.
142+ * `null` if there's no preference.
143+ */
144+ video : string | null ;
145+ } ;
146+
116147 constructor ( args : {
117148 preferTrickModeTracks : boolean ;
118149 defaultAudioTrackSwitchingMode : IAudioTrackSwitchingMode | undefined ;
@@ -133,6 +164,10 @@ export default class TracksStore extends EventEmitter<ITracksStoreEvents> {
133164 args . defaultAudioTrackSwitchingMode ??
134165 config . getCurrent ( ) . DEFAULT_AUDIO_TRACK_SWITCHING_MODE ;
135166 this . onTracksNotPlayableForType = args . onTracksNotPlayableForType ;
167+ this . _preferredCodecFamily = {
168+ audio : null ,
169+ video : null ,
170+ } ;
136171 }
137172
138173 /**
@@ -386,12 +421,8 @@ export default class TracksStore extends EventEmitter<ITracksStoreEvents> {
386421 }
387422
388423 const periodItem = getPeriodItem ( this . _storedPeriodInfo , periodInfo . period . id ) ;
389- if (
390- periodItem !== undefined &&
391- periodItem . isPeriodAdvertised &&
392- periodItem [ type ] . storedSettings === null
393- ) {
394- periodItem [ type ] . dispatcher ?. updateTrack ( null ) ;
424+ if ( periodItem !== undefined && periodItem . isPeriodAdvertised ) {
425+ this . _dispatchTrackSetting ( periodItem [ type ] , null ) ;
395426 }
396427 }
397428
@@ -543,14 +574,50 @@ export default class TracksStore extends EventEmitter<ITracksStoreEvents> {
543574 ! trackObj . dispatcher . hasSetTrack ( ) &&
544575 trackObj . storedSettings !== undefined
545576 ) {
546- trackObj . dispatcher . updateTrack ( trackObj . storedSettings ) ;
577+ // Just ensure we set it
578+ this . _dispatchTrackSetting ( trackObj , trackObj . storedSettings ) ;
547579 }
548580 if ( this . _isDisposed ) {
549581 return ;
550582 }
551583 }
552584 }
553585
586+ /**
587+ * Update the track chosen by signaling it to the rest of the RxPlayer.
588+ *
589+ * Takes a `checkedSettings` argument as a safeguard: there's many API and
590+ * re-entrancy potential that easily lead to state desynchonization in our track
591+ * handling logic, so the idea is to re-check with what you expect the "settings"
592+ * to be: if not equal it will be assumed that another piece of code already took
593+ * care of track update since.
594+ *
595+ * @param {Object } periodInfo - The track object for the corresponding Period/type.
596+ * @param {Object } checkedSetting - The setting expected to be set. `null` for no
597+ * track. Will be re-checked against the state.
598+ */
599+ private _dispatchTrackSetting (
600+ periodInfo : IVideoPeriodInfo | IAudioPeriodInfo | ITextPeriodInfo ,
601+ checkedSetting :
602+ IVideoStoredSettings | IAudioStoredSettings | ITextStoredSettings | null ,
603+ ) : void {
604+ if (
605+ this . _isDisposed ||
606+ periodInfo . dispatcher === null ||
607+ periodInfo . storedSettings !== checkedSetting
608+ ) {
609+ return ;
610+ }
611+
612+ if ( periodInfo . type === "video" || periodInfo . type === "audio" ) {
613+ this . _preferredCodecFamily [ periodInfo . type ] =
614+ checkedSetting === null
615+ ? null
616+ : ( getCodecFamily ( checkedSetting . adaptation ) ?? null ) ;
617+ }
618+ periodInfo . dispatcher . updateTrack ( checkedSetting ) ;
619+ }
620+
554621 /**
555622 * Throws an error if neither audio nor video tracks are selected for the given period.
556623 *
@@ -642,11 +709,7 @@ export default class TracksStore extends EventEmitter<ITracksStoreEvents> {
642709 if ( this . _isDisposed ) {
643710 return ; // Someone disposed the `TracksStore` on the previous side-effect
644711 }
645-
646- // Check again that no track change occurred in the meantime
647- if ( typeInfo . storedSettings === storedSettings ) {
648- typeInfo . dispatcher ?. updateTrack ( storedSettings ) ;
649- }
712+ this . _dispatchTrackSetting ( typeInfo , storedSettings ) ;
650713 } else if ( fallbackTrack === null && ! noSourceMedia ) {
651714 this . trigger ( "noPlayableTrack" , {
652715 trackType,
@@ -676,14 +739,7 @@ export default class TracksStore extends EventEmitter<ITracksStoreEvents> {
676739 reason : "no-playable-representation" ,
677740 } ) ;
678741 }
679- if ( typeInfo . storedSettings !== null || this . _isDisposed ) {
680- // The previous "trackUpdate" event might have caused changes,
681- // so we re-check to see if the selected track has been updated.
682- // If it has, we exit early because the API consumer likely adjusted the settings,
683- // and throwing an error now would be out of sync with their changes.
684- } else {
685- typeInfo . dispatcher ?. updateTrack ( null ) ;
686- }
742+ this . _dispatchTrackSetting ( typeInfo , null ) ;
687743 } else if ( fallbackBehavior === "error" ) {
688744 const noRepErr = new MediaError (
689745 "NO_PLAYABLE_REPRESENTATION" ,
@@ -707,14 +763,7 @@ export default class TracksStore extends EventEmitter<ITracksStoreEvents> {
707763 reason : "no-playable-representation" ,
708764 } ) ;
709765 }
710- if ( typeInfo . storedSettings !== null || this . _isDisposed ) {
711- // The previous "trackUpdate" event might have caused changes,
712- // so we re-check to see if the selected track has been updated.
713- // If it has, we exit early because the API consumer likely adjusted the settings,
714- // and throwing an error now would be out of sync with their changes.
715- } else {
716- typeInfo . dispatcher ?. updateTrack ( null ) ;
717- }
766+ this . _dispatchTrackSetting ( typeInfo , null ) ;
718767 }
719768
720769 // The previous event trigger could have had side-effects, so we
@@ -994,11 +1043,9 @@ export default class TracksStore extends EventEmitter<ITracksStoreEvents> {
9941043 return ; // Someone disposed the `TracksStore` on the previous side-effect
9951044 }
9961045 const newPeriodItem = getPeriodItem ( this . _storedPeriodInfo , period . id ) ;
997- if (
998- newPeriodItem !== undefined &&
999- newPeriodItem [ bufferType ] . storedSettings === storedSettings
1000- ) {
1001- newPeriodItem [ bufferType ] . dispatcher ?. updateTrack ( storedSettings ) ;
1046+ const trackObj = newPeriodItem ?. [ bufferType ] ;
1047+ if ( trackObj !== undefined ) {
1048+ this . _dispatchTrackSetting ( trackObj , storedSettings ) ;
10021049 }
10031050 }
10041051
@@ -1086,11 +1133,8 @@ export default class TracksStore extends EventEmitter<ITracksStoreEvents> {
10861133 return ; // Someone disposed the `TracksStore` on the previous side-effect
10871134 }
10881135 const newPeriodItem = getPeriodItem ( this . _storedPeriodInfo , period . id ) ;
1089- if (
1090- newPeriodItem !== undefined &&
1091- newPeriodItem . video . storedSettings === storedSettings
1092- ) {
1093- newPeriodItem . video . dispatcher ?. updateTrack ( storedSettings ) ;
1136+ if ( newPeriodItem !== undefined ) {
1137+ this . _dispatchTrackSetting ( newPeriodItem . video , storedSettings ) ;
10941138 }
10951139 }
10961140
@@ -1131,11 +1175,9 @@ export default class TracksStore extends EventEmitter<ITracksStoreEvents> {
11311175 return ; // Someone disposed the `TracksStore` on the previous side-effect
11321176 }
11331177 const newPeriodItem = getPeriodItem ( this . _storedPeriodInfo , periodObj . period . id ) ;
1134- if (
1135- newPeriodItem !== undefined &&
1136- newPeriodItem [ bufferType ] . storedSettings === null
1137- ) {
1138- newPeriodItem [ bufferType ] . dispatcher ?. updateTrack ( null ) ;
1178+ const trackObj = newPeriodItem ?. [ bufferType ] ;
1179+ if ( trackObj !== undefined ) {
1180+ this . _dispatchTrackSetting ( trackObj , null ) ;
11391181 }
11401182
11411183 if ( newPeriodItem !== undefined ) {
@@ -1451,12 +1493,8 @@ export default class TracksStore extends EventEmitter<ITracksStoreEvents> {
14511493 return ; // Someone disposed the `TracksStore` on the previous side-effect
14521494 }
14531495 const newPeriodItem = getPeriodItem ( this . _storedPeriodInfo , period . id ) ;
1454- if (
1455- newPeriodItem !== undefined &&
1456- newPeriodItem . isPeriodAdvertised &&
1457- newPeriodItem . video . storedSettings === storedSettings
1458- ) {
1459- newPeriodItem . video . dispatcher ?. updateTrack ( storedSettings ) ;
1496+ if ( newPeriodItem !== undefined && newPeriodItem . isPeriodAdvertised ) {
1497+ this . _dispatchTrackSetting ( newPeriodItem . video , storedSettings ) ;
14601498 }
14611499 }
14621500 }
@@ -1527,10 +1565,11 @@ export default class TracksStore extends EventEmitter<ITracksStoreEvents> {
15271565 continue ;
15281566 }
15291567
1530- const audioAdaptation : IAdaptationMetadata | undefined = getSupportedAdaptations (
1568+ const audioAdaptation = getInitialAdaptation (
15311569 period ,
15321570 "audio" ,
1533- ) [ 0 ] ;
1571+ this . _preferredCodecFamily . audio ,
1572+ ) ;
15341573 if ( audioAdaptation === undefined ) {
15351574 trackStorePeriod . audio . storedSettings = null ;
15361575 this . handleMissingOrUnplayableTrack ( period , "audio" , true ) ;
@@ -1545,8 +1584,11 @@ export default class TracksStore extends EventEmitter<ITracksStoreEvents> {
15451584 } ;
15461585 }
15471586
1548- const baseVideoAdaptation : IAdaptationMetadata | undefined =
1549- getSupportedAdaptations ( period , "video" ) [ 0 ] ;
1587+ const baseVideoAdaptation = getInitialAdaptation (
1588+ period ,
1589+ "video" ,
1590+ this . _preferredCodecFamily . video ,
1591+ ) ;
15501592 if ( baseVideoAdaptation === undefined ) {
15511593 trackStorePeriod . video . storedSettings = null ;
15521594 this . handleMissingOrUnplayableTrack ( period , "video" , true ) ;
@@ -1623,7 +1665,7 @@ export default class TracksStore extends EventEmitter<ITracksStoreEvents> {
16231665 trackInfo . storedSettings !== undefined &&
16241666 ! trackInfo . dispatcher . hasSetTrack ( )
16251667 ) {
1626- trackInfo . dispatcher . updateTrack ( trackInfo . storedSettings ) ;
1668+ this . _dispatchTrackSetting ( trackInfo , trackInfo . storedSettings ) ;
16271669 if ( this . _isDisposed ) {
16281670 return ;
16291671 }
@@ -1725,9 +1767,9 @@ function generatePeriodInfo(
17251767 inManifest,
17261768 isPeriodAdvertised : false ,
17271769 isRemoved : false ,
1728- audio : { storedSettings : undefined , dispatcher : null } ,
1729- video : { storedSettings : undefined , dispatcher : null } ,
1730- text : { storedSettings : undefined , dispatcher : null } ,
1770+ audio : { type : "audio" , storedSettings : undefined , dispatcher : null } ,
1771+ video : { type : "video" , storedSettings : undefined , dispatcher : null } ,
1772+ text : { type : "text" , storedSettings : undefined , dispatcher : null } ,
17311773 } ;
17321774}
17331775
@@ -1833,6 +1875,7 @@ export interface ITSPeriodObject {
18331875 * the Manifest.
18341876 */
18351877interface IAudioPeriodInfo {
1878+ type : "audio" ;
18361879 /**
18371880 * Information on the last audio track settings wanted by the user.
18381881 * `null` if no audio track is wanted.
@@ -1869,26 +1912,13 @@ interface IAudioStoredSettings {
18691912 * the Manifest.
18701913 */
18711914export interface ITextPeriodInfo {
1915+ type : "text" ;
18721916 /**
18731917 * Information on the last text track settings wanted.
18741918 * `null` if no text track is wanted.
18751919 * `undefined` if not set yet.
18761920 */
1877- storedSettings :
1878- | {
1879- /** Contains the last `Adaptation` wanted by the user. */
1880- adaptation : IAdaptationMetadata ;
1881- /** "Switching mode" in which the track switch should happen. */
1882- switchingMode : "direct" ;
1883- /**
1884- * Contains the last locked `Representation`s for this `Adaptation` wanted
1885- * by the user.
1886- * `null` if no Representation is locked.
1887- */
1888- lockedRepresentations : SharedReference < IRepresentationsChoice | null > ;
1889- }
1890- | null
1891- | undefined ;
1921+ storedSettings : ITextStoredSettings | null | undefined ;
18921922 /**
18931923 * Tracks are internally emitted through RxJS's `Subject`s.
18941924 * A `TrackDispatcher` allows to facilitate and centralize the management of
@@ -1901,11 +1931,25 @@ export interface ITextPeriodInfo {
19011931 dispatcher : TrackDispatcher | null ;
19021932}
19031933
1934+ interface ITextStoredSettings {
1935+ /** Contains the last `Adaptation` wanted by the user. */
1936+ adaptation : IAdaptationMetadata ;
1937+ /** "Switching mode" in which the track switch should happen. */
1938+ switchingMode : "direct" ;
1939+ /**
1940+ * Contains the last locked `Representation`s for this `Adaptation` wanted
1941+ * by the user.
1942+ * `null` if no Representation is locked.
1943+ */
1944+ lockedRepresentations : SharedReference < IRepresentationsChoice | null > ;
1945+ }
1946+
19041947/**
19051948 * Internal representation of video track preferences for a given `Period` of
19061949 * the Manifest.
19071950 */
19081951export interface IVideoPeriodInfo {
1952+ type : "video" ;
19091953 /**
19101954 * Information on the `id` of the last video track settings wanted.
19111955 * `null` if no video track is wanted.
@@ -1967,3 +2011,54 @@ export interface IVideoRepresentationsLockSettings {
19672011 representations : string [ ] ;
19682012 switchingMode ?: IVideoRepresentationsSwitchingMode | undefined ;
19692013}
2014+
2015+ /**
2016+ * Determine a good initial Adaptation to start from in the given Period when
2017+ * you don't have a preference.
2018+ * @param {Object } period - The period Object on which `Adaptation` objects are
2019+ * announced.
2020+ * @param {string } trackType - e.g. "audio" or "video".
2021+ * @param {string|null } preferredCodecFamily - If set, you would prefer the
2022+ * Adaptation to have the given "codec family" (initial part of the codec
2023+ * identifier, e.g. `avc1`), but no obligation.
2024+ * @returns {Object|undefined } - The selected initial track. If `undefined`,
2025+ * there's no playable track in that Period for that type.
2026+ */
2027+ function getInitialAdaptation (
2028+ period : IPeriodMetadata ,
2029+ trackType : ITrackType ,
2030+ preferredCodecFamily : string | null ,
2031+ ) : IAdaptationMetadata | undefined {
2032+ const supportedAdaptations = getSupportedAdaptations ( period , trackType ) ;
2033+
2034+ let adaptation : IAdaptationMetadata | undefined ;
2035+ if ( preferredCodecFamily !== null ) {
2036+ adaptation = arrayFind (
2037+ supportedAdaptations ,
2038+ ( a ) => getCodecFamily ( a ) === preferredCodecFamily ,
2039+ ) ;
2040+ }
2041+ return adaptation ?? supportedAdaptations [ 0 ] ;
2042+ }
2043+
2044+ /**
2045+ * From the given track, returns what we will call its "codec family".
2046+ * This allows to return e.g. `avc1`, `hvc1`, `vp9` etc. without profile and
2047+ * level considerations.
2048+ * Returns `undefined` if this cannot be determined.
2049+ *
2050+ * The logic is only a naive algorithm and may produce false negatives
2051+ * @param {Object } a - The track to extract the codec family from.
2052+ * @returns {string|undefined }
2053+ */
2054+ function getCodecFamily ( a : IAdaptationMetadata ) : string | undefined {
2055+ const codec = a . representations [ 0 ] ?. chosenCodec ;
2056+ if ( codec === undefined || codec === "" ) {
2057+ return undefined ;
2058+ }
2059+ const initialPart = codec . split ( "." ) [ 0 ] ;
2060+ if ( initialPart === undefined || initialPart === "" ) {
2061+ return undefined ;
2062+ }
2063+ return initialPart ;
2064+ }
0 commit comments