diff --git a/externs/audiotrack.js b/externs/audiotrack.js new file mode 100644 index 0000000000..df2ee5b516 --- /dev/null +++ b/externs/audiotrack.js @@ -0,0 +1,67 @@ +/** + * @license + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Externs for AudioTrack which are missing from the Closure + * compiler. + * + * @externs + */ + +/** @constructor */ +function AudioTrack() {} + +/** @type {boolean} */ +AudioTrack.prototype.enabled; + +/** @type {string} */ +AudioTrack.prototype.id; + +/** @type {string} */ +AudioTrack.prototype.kind; + +/** @type {string} */ +AudioTrack.prototype.label; + +/** @type {string} */ +AudioTrack.prototype.language; + +/** @type {SourceBuffer} */ +AudioTrack.prototype.sourceBuffer; + + +/** + * @extends {IArrayLike.} + * @extends {EventTarget} + * @interface + */ +function AudioTrackList() {} + +/** @override */ +AudioTrackList.prototype.addEventListener = + function(type, listener, useCapture) {}; + +/** @override */ +AudioTrackList.prototype.removeEventListener = + function(type, listener, useCapture) {}; + +/** @override */ +AudioTrackList.prototype.dispatchEvent = function(event) {}; + + +/** @type {AudioTrackList} */ +HTMLMediaElement.prototype.audioTracks; diff --git a/externs/shaka/player.js b/externs/shaka/player.js index 06200eda3e..c1e7a8cb3c 100644 --- a/externs/shaka/player.js +++ b/externs/shaka/player.js @@ -191,6 +191,7 @@ shaka.extern.BufferedInfo; * videoCodec: ?string, * primary: boolean, * roles: !Array., + * audioRoles: Array., * videoId: ?number, * audioId: ?number, * channelsCount: ?number, @@ -248,6 +249,9 @@ shaka.extern.BufferedInfo; * cannot be satisfied. * @property {!Array.} roles * The roles of the track, e.g. 'main', 'caption', or 'commentary'. + * @property {Array.} audioRoles + * The roles of the audio in the track, e.g. 'main' or 'commentary'. + * Will be null for text tracks or variant tracks without audio. * @property {?number} videoId * (only for variant tracks) The video stream id. * @property {?number} audioId diff --git a/externs/texttrack.js b/externs/texttrack.js index b363e20dd7..3dfe78e14d 100644 --- a/externs/texttrack.js +++ b/externs/texttrack.js @@ -17,11 +17,16 @@ /** * @fileoverview Externs for TextTrack and TextTrackCue which are - * missing from the closure compiler. + * missing from the Closure compiler. * * @externs */ +/** @type {string} */ +TextTrack.prototype.id; + +/** @type {string} */ +TextTrack.prototype.kind; /** @type {string} */ TextTrack.prototype.label; @@ -30,18 +35,14 @@ TextTrack.prototype.label; /** @type {string} */ TextTrackCue.prototype.positionAlign; - /** @type {string} */ TextTrackCue.prototype.lineAlign; - /** @type {number|null|string} */ TextTrackCue.prototype.line; - /** @type {string} */ TextTrackCue.prototype.vertical; - /** @type {boolean} */ TextTrackCue.prototype.snapToLines; diff --git a/externs/videotrack.js b/externs/videotrack.js new file mode 100644 index 0000000000..ff5b085207 --- /dev/null +++ b/externs/videotrack.js @@ -0,0 +1,67 @@ +/** + * @license + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Externs for VideoTrack which are missing from the Closure + * compiler. + * + * @externs + */ + +/** @constructor */ +function VideoTrack() {} + +/** @type {boolean} */ +VideoTrack.prototype.selected; + +/** @type {string} */ +VideoTrack.prototype.id; + +/** @type {string} */ +VideoTrack.prototype.kind; + +/** @type {string} */ +VideoTrack.prototype.label; + +/** @type {string} */ +VideoTrack.prototype.language; + +/** @type {SourceBuffer} */ +VideoTrack.prototype.sourceBuffer; + + +/** + * @extends {IArrayLike.} + * @extends {EventTarget} + * @interface + */ +function VideoTrackList() {} + +/** @override */ +VideoTrackList.prototype.addEventListener = + function(type, listener, useCapture) {}; + +/** @override */ +VideoTrackList.prototype.removeEventListener = + function(type, listener, useCapture) {}; + +/** @override */ +VideoTrackList.prototype.dispatchEvent = function(event) {}; + + +/** @type {VideoTrackList} */ +HTMLMediaElement.prototype.videoTracks; diff --git a/lib/player.js b/lib/player.js index fada633f27..586fe554d7 100644 --- a/lib/player.js +++ b/lib/player.js @@ -1901,6 +1901,25 @@ shaka.Player.prototype.onSrcEquals_ = function(has, wants) { this.stats_.setLoadLatency(delta); }); + // The audio tracks are only available on Safari at the moment, but this + // drives the tracks API for Safari's native HLS. So when they change, + // fire the corresponding Shaka Player event. + if (this.video_.audioTracks) { + this.eventManager_.listen( + this.video_.audioTracks, 'addtrack', () => this.onTracksChanged_()); + this.eventManager_.listen( + this.video_.audioTracks, 'removetrack', () => this.onTracksChanged_()); + } + if (this.video_.textTracks) { + // This is a real EventTarget, but the compiler doesn't know that. + // TODO: File a bug or send a PR to the compiler externs to fix this. + const textTracks = /** @type {EventTarget} */(this.video_.textTracks); + this.eventManager_.listen( + textTracks, 'addtrack', () => this.onTracksChanged_()); + this.eventManager_.listen( + textTracks, 'removetrack', () => this.onTracksChanged_()); + } + // By setting |src| we are done "loading" with src=. We don't need to set the // current time because |playhead| will do that for us. has.mediaElement.src = has.uri; @@ -1913,8 +1932,6 @@ shaka.Player.prototype.onSrcEquals_ = function(has, wants) { // streaming. But we should fire it in this path anyway since some // applications may be expecting it as a life-cycle event. this.dispatchEvent(new shaka.util.FakeEvent('streaming')); - // Dispatch a 'trackschanged' event, for the same reasons as 'streaming'. - this.onTracksChanged_(); // This is fully loaded when we have loaded the first frame. const fullyLoaded = new shaka.util.PublicPromise(); @@ -2784,8 +2801,14 @@ shaka.Player.prototype.getVariantTracks = function() { } return tracks; + } else if (this.video_ && this.video_.audioTracks) { + // Safari's native HLS always shows a single element in videoTracks. + // You can't use that API to change resolutions. But we can use audioTracks + // to generate a variant list that is usable for changing languages. + const audioTracks = Array.from(this.video_.audioTracks); + return audioTracks.map((audio) => + shaka.util.StreamUtils.html5AudioTrackToTrack(audio)); } else { - // TODO: Safari's native HLS has audioTracks/videoTracks on the element. return []; } }; @@ -2815,8 +2838,11 @@ shaka.Player.prototype.getTextTracks = function() { } return tracks; + } else if (this.video_ && this.video_.src && this.video_.textTracks) { + const textTracks = Array.from(this.video_.textTracks); + const StreamUtils = shaka.util.StreamUtils; + return textTracks.map((text) => StreamUtils.html5TextTrackToTrack(text)); } else { - // TODO: Safari's native HLS has textTracks on HTMLMediaElement. return []; } }; @@ -2854,8 +2880,19 @@ shaka.Player.prototype.selectTextTrack = function(track) { // When track is selected, back-propogate the language to // currentTextLanguage_. this.currentTextLanguage_ = stream.language; - } else { - // TODO: Safari's native HLS has textTracks on HTMLMediaElement. + } else if (this.video_ && this.video_.src && this.video_.textTracks) { + const textTracks = Array.from(this.video_.textTracks); + for (const textTrack of textTracks) { + if (shaka.util.StreamUtils.html5TrackId(textTrack) == track.id) { + // Leave the track in 'hidden' if it's selected but not showing. + textTrack.mode = this.isTextVisible_ ? 'showing' : 'hidden'; + } else { + // Safari allows multiple text tracks to have mode == 'showing', so be + // explicit in resetting the others. + textTrack.mode = 'disabled'; + } + } + this.onTextChanged_(); } }; @@ -2979,8 +3016,17 @@ shaka.Player.prototype.selectVariantTrack = function( // Update AbrManager variants to match these new settings. this.chooseVariant_(period.variants); - } else { - // TODO: Safari's native HLS has audioTracks/vidoeTracks on the element. + } else if (this.video_ && this.video_.audioTracks) { + // Safari's native HLS won't let you choose an explicit variant, though you + // can choose audio languages this way. + const audioTracks = Array.from(this.video_.audioTracks); + for (const audioTrack of audioTracks) { + if (shaka.util.StreamUtils.html5TrackId(audioTrack) == track.id) { + // This will reset the "enabled" of other tracks to false. + audioTrack.enabled = true; + } + } + this.onVariantChanged_(); } }; @@ -2994,24 +3040,7 @@ shaka.Player.prototype.selectVariantTrack = function( * @export */ shaka.Player.prototype.getAudioLanguagesAndRoles = function() { - // TODO: This assumes that language is always on the audio stream. This is not - // true when audio and video are muxed together. - // TODO: If the language is on the video stream, how do roles affect the - // the language-role pairing? - - // TODO: Make generic through variant tracks instead - if (this.manifest_ && this.playhead_) { - /** @type {!Array.} */ - const audioStreams = []; - for (const variant of this.getSelectableVariants_()) { - audioStreams.push(variant.audio); - } - - return shaka.Player.getLanguageAndRolesFrom_(audioStreams); - } else { - // TODO: Safari's native HLS has audioTracks on HTMLMediaElement. - return []; - } + return shaka.Player.getLanguageAndRolesFrom_(this.getVariantTracks()); }; @@ -3024,13 +3053,7 @@ shaka.Player.prototype.getAudioLanguagesAndRoles = function() { * @export */ shaka.Player.prototype.getTextLanguagesAndRoles = function() { - // TODO: Make generic through text tracks instead - if (this.manifest_ && this.playhead_) { - return shaka.Player.getLanguageAndRolesFrom_(this.getSelectableText_()); - } else { - // TODO: Safari's native HLS has textTracks on HTMLMediaElement. - return []; - } + return shaka.Player.getLanguageAndRolesFrom_(this.getTextTracks()); }; @@ -3042,22 +3065,7 @@ shaka.Player.prototype.getTextLanguagesAndRoles = function() { * @export */ shaka.Player.prototype.getAudioLanguages = function() { - // TODO: This assumes that language is always on the audio stream. This is not - // true when audio and video are muxed together. - - // TODO: Make generic through variant tracks instead - if (this.manifest_ && this.playhead_) { - /** @type {!Array.} */ - const audioStreams = []; - for (const variant of this.getSelectableVariants_()) { - audioStreams.push(variant.audio); - } - - return Array.from(shaka.Player.getLanguagesFrom_(audioStreams)); - } else { - // TODO: Safari's native HLS has audioTracks on HTMLMediaElement. - return []; - } + return Array.from(shaka.Player.getLanguagesFrom_(this.getVariantTracks())); }; @@ -3069,14 +3077,7 @@ shaka.Player.prototype.getAudioLanguages = function() { * @export */ shaka.Player.prototype.getTextLanguages = function() { - // TODO: Make generic through text tracks instead - if (this.manifest_ && this.playhead_) { - return Array.from( - shaka.Player.getLanguagesFrom_(this.getSelectableText_())); - } else { - // TODO: Safari's native HLS has textTracks on HTMLMediaElement. - return []; - } + return Array.from(shaka.Player.getLanguagesFrom_(this.getTextTracks())); }; @@ -3099,8 +3100,14 @@ shaka.Player.prototype.selectAudioLanguage = function(language, role) { // TODO: Refactor to only change audio and not affect text. this.chooseStreamsAndSwitch_(period); - } else { - // TODO: Safari's native HLS has audioTracks on HTMLMediaElement. + } else if (this.video_ && this.video_.audioTracks) { + const audioTracks = Array.from(this.video_.audioTracks); + for (const audioTrack of audioTracks) { + if (audioTrack.language == language) { + // This will reset the "enabled" of other tracks to false. + audioTrack.enabled = true; + } + } } }; @@ -3124,7 +3131,10 @@ shaka.Player.prototype.selectTextLanguage = function(language, role) { // TODO: Refactor to only change text and not affect audio. this.chooseStreamsAndSwitch_(period); } else { - // TODO: Safari's native HLS has textTracks on HTMLMediaElement. + const track = this.getTextTracks().filter((t) => t.language == language)[0]; + if (track) { + this.selectTextTrack(track); + } } }; @@ -3147,8 +3157,9 @@ shaka.Player.prototype.isTextTrackVisible = function() { // Always return the actual value so that the app has the most accurate // information (in the case that the values come out of sync in prod). return actual; - } else { - // TODO: Safari's native HLS has textTracks on HTMLMediaElement. + } else if (this.video_ && this.video_.src && this.video_.textTracks) { + const textTracks = Array.from(this.video_.textTracks); + return textTracks.some((t) => t.mode == 'showing'); } return expected; @@ -3224,8 +3235,17 @@ shaka.Player.prototype.updateTextVisibility_ = async function(isVisible) { } await this.streamingEngine_.loadNewTextStream(streams[0]); - } else { // if (this.loadMode_ == shaka.Player.LoadMode.SRC_EQUALS) { - // TODO: Safari's native HLS has textTracks on HTMLMediaElement. + } else if (this.video_ && this.video_.src && this.video_.textTracks) { + const textTracks = Array.from(this.video_.textTracks); + + // Find the active track by looking for one which is not disabled. This is + // the only way to identify the track which is currently displayed. + // Set it to 'showing' or 'hidden' based on isVisible. + for (const textTrack of textTracks) { + if (textTrack.mode != 'disabled') { + textTrack.mode = isVisible ? 'showing' : 'hidden'; + } + } } }; @@ -3249,8 +3269,18 @@ shaka.Player.prototype.getPlayheadTimeAsDate = function() { const startTime = timeline.getPresentationStartTime(); const presentationTime = this.video_.currentTime; return new Date(/* ms= */ (startTime + presentationTime) * 1000); + } else if (this.video_ && this.video_.getStartDate) { + // Apple's native HLS gives us getStartDate(), which is only available if + // EXT-X-PROGRAM-DATETIME is in the playlist. + const startDate = this.video_.getStartDate(); + if (isNaN(startDate.getTime())) { + shaka.log.warning( + 'EXT-X-PROGRAM-DATETIME required to get playhead time as Date!'); + return null; + } + return new Date(startDate.getTime() + (this.video_.currentTime * 1000)); } else { - shaka.log.warning('No way to get playhead time as date!'); + shaka.log.warning('No way to get playhead time as Date!'); return null; } }; @@ -3274,8 +3304,19 @@ shaka.Player.prototype.getPresentationStartTimeAsDate = function() { const timeline = this.manifest_.presentationTimeline; const startTime = timeline.getPresentationStartTime(); return new Date(/* ms= */ startTime * 1000); + } else if (this.video_ && this.video_.getStartDate) { + // Apple's native HLS gives us getStartDate(), which is only available if + // EXT-X-PROGRAM-DATETIME is in the playlist. + const startDate = this.video_.getStartDate(); + if (isNaN(startDate.getTime())) { + shaka.log.warning( + 'EXT-X-PROGRAM-DATETIME required to get presentation start time as ' + + 'Date!'); + return null; + } + return startDate; } else { - shaka.log.warning('No way to get presentation start time as date!'); + shaka.log.warning('No way to get presentation start time as Date!'); return null; } }; @@ -4359,7 +4400,7 @@ shaka.Player.prototype.onTracksChanged_ = function() { * @private */ shaka.Player.prototype.onVariantChanged_ = function() { - // Delay the 'trackschanged' event so StreamingEngine has time to absorb the + // Delay the 'variantchanged' event so StreamingEngine has time to absorb the // changes before the user tries to query it. this.delayDispatchEvent_(new shaka.util.FakeEvent('variantchanged')); }; @@ -4716,20 +4757,18 @@ shaka.Player.prototype.delayDispatchEvent_ = async function(event) { }; /** - * Get the normalized languages for a group of streams. If a stream is |null|, - * it means that there is a variant but no audio stream and the language should - * be "und". + * Get the normalized languages for a group of tracks. * - * @param {!Array.} streams + * @param {!Array.} tracks * @return {!Set.} * @private */ -shaka.Player.getLanguagesFrom_ = function(streams) { +shaka.Player.getLanguagesFrom_ = function(tracks) { const languages = new Set(); - for (const stream of streams) { - if (stream && stream.language) { - languages.add(shaka.util.LanguageUtils.normalize(stream.language)); + for (const track of tracks) { + if (track.language) { + languages.add(shaka.util.LanguageUtils.normalize(track.language)); } else { languages.add('und'); } @@ -4740,31 +4779,34 @@ shaka.Player.getLanguagesFrom_ = function(streams) { /** - * Get all permutations of normalized languages and role for a group of streams. - * If a stream is |null|, it means that there is a variant but no audio stream - * and the language should be "und". + * Get all permutations of normalized languages and role for a group of tracks. * - * @param {!Array.} streams + * @param {!Array.} tracks * @return {!Array.} * @private */ -shaka.Player.getLanguageAndRolesFrom_ = function(streams) { +shaka.Player.getLanguageAndRolesFrom_ = function(tracks) { /** @type {!Map.} */ const languageToRoles = new Map(); - // We must have an empty role so that we will still get a language-role entry. - const noRoles = ['']; - - for (const stream of streams) { + for (const track of tracks) { let language = 'und'; - let roles = noRoles; + let roles = []; - if (stream && stream.language) { - language = shaka.util.LanguageUtils.normalize(stream.language); + if (track.language) { + language = shaka.util.LanguageUtils.normalize(track.language); + } + + if (track.type == 'variant') { + roles = track.audioRoles; + } else { + roles = track.roles; } - if (stream && stream.roles.length) { - roles = stream.roles; + if (!roles || !roles.length) { + // We must have an empty role so that we will still get a language-role + // entry from our Map. + roles = ['']; } if (!languageToRoles.has(language)) { diff --git a/lib/util/stream_utils.js b/lib/util/stream_utils.js index 7355ba4350..fb26115355 100644 --- a/lib/util/stream_utils.js +++ b/lib/util/stream_utils.js @@ -278,6 +278,7 @@ shaka.util.StreamUtils.variantToTrack = function(variant) { videoCodec: videoCodec, primary: variant.primary, roles: Array.from(roles), + audioRoles: null, videoId: null, audioId: null, channelsCount: null, @@ -303,6 +304,7 @@ shaka.util.StreamUtils.variantToTrack = function(variant) { track.channelsCount = audio.channelsCount; track.audioBandwidth = audio.bandwidth || null; track.label = audio.label; + track.audioRoles = audio.roles; } return track; @@ -334,6 +336,7 @@ shaka.util.StreamUtils.textStreamToTrack = function(stream) { videoCodec: null, primary: stream.primary, roles: stream.roles, + audioRoles: null, videoId: null, audioId: null, channelsCount: null, @@ -348,6 +351,113 @@ shaka.util.StreamUtils.textStreamToTrack = function(stream) { }; +/** + * Generate and return an ID for this track, since the ID field is optional. + * + * @param {TextTrack|AudioTrack} html5Track + * @return {number} The generated ID. + */ +shaka.util.StreamUtils.html5TrackId = function(html5Track) { + if (!html5Track['__shaka_id']) { + html5Track['__shaka_id'] = shaka.util.StreamUtils.nextTrackId_++; + } + return html5Track['__shaka_id']; +}; + + +/** @private {number} */ +shaka.util.StreamUtils.nextTrackId_ = 0; + + +/** + * @param {TextTrack} textTrack + * @return {shaka.extern.Track} + */ +shaka.util.StreamUtils.html5TextTrackToTrack = function(textTrack) { + const CLOSED_CAPTION_MIMETYPE = shaka.util.MimeUtils.CLOSED_CAPTION_MIMETYPE; + const StreamUtils = shaka.util.StreamUtils; + + /** @type {shaka.extern.Track} */ + const track = StreamUtils.html5TrackToGenericShakaTrack_(textTrack); + track.active = textTrack.mode != 'disabled'; + track.type = 'text'; + track.originalTextId = textTrack.id; + if (textTrack.kind == 'captions') { + track.mimeType = CLOSED_CAPTION_MIMETYPE; + } + + return track; +}; + + +/** + * @param {AudioTrack} audioTrack + * @return {shaka.extern.Track} + */ +shaka.util.StreamUtils.html5AudioTrackToTrack = function(audioTrack) { + const StreamUtils = shaka.util.StreamUtils; + + /** @type {shaka.extern.Track} */ + const track = StreamUtils.html5TrackToGenericShakaTrack_(audioTrack); + track.active = audioTrack.enabled; + track.type = 'variant'; + track.originalAudioId = audioTrack.id; + + if (audioTrack.kind == 'main') { + track.primary = true; + track.roles = ['main']; + track.audioRoles = ['main']; + } else { + track.audioRoles = []; + } + + return track; +}; + + +/** + * Creates a Track object with non-type specific fields filled out. The caller + * is responsible for completing the Track object with any type-specific + * information (audio or text). + * + * @param {TextTrack|AudioTrack} html5Track + * @return {shaka.extern.Track} + * @private + */ +shaka.util.StreamUtils.html5TrackToGenericShakaTrack_ = function(html5Track) { + /** @type {shaka.extern.Track} */ + const track = { + id: shaka.util.StreamUtils.html5TrackId(html5Track), + active: false, + type: '', + bandwidth: 0, + language: shaka.util.LanguageUtils.normalize(html5Track.language), + label: html5Track.label, + kind: html5Track.kind, + width: null, + height: null, + frameRate: null, + mimeType: null, + codecs: null, + audioCodec: null, + videoCodec: null, + primary: false, + roles: [], + audioRoles: null, + videoId: null, + audioId: null, + channelsCount: null, + audioBandwidth: null, + videoBandwidth: null, + originalVideoId: null, + originalAudioId: null, + originalTextId: null, + }; + + return track; +}; + + /** * Determines if the given variant is playable. * @param {!shaka.extern.Variant} variant diff --git a/test/offline/storage_unit.js b/test/offline/storage_unit.js index 099666fedf..df7014065a 100644 --- a/test/offline/storage_unit.js +++ b/test/offline/storage_unit.js @@ -1188,6 +1188,7 @@ describe('Storage', function() { videoCodec: 'mp4', primary: false, roles: [], + audioRoles: [], videoId: videoId, audioId: audioId, channelsCount: 2, @@ -1222,6 +1223,7 @@ describe('Storage', function() { videoCodec: null, primary: false, roles: [], + audioRoles: null, videoId: null, audioId: null, channelsCount: null, diff --git a/test/player_src_equals_integration.js b/test/player_src_equals_integration.js index ceecb93a78..4f5bfd0644 100644 --- a/test/player_src_equals_integration.js +++ b/test/player_src_equals_integration.js @@ -70,17 +70,16 @@ describe('Player Src Equals', () => { expect(player.getAssetUri()).toBe(SMALL_MP4_CONTENT_URI); }); - // Since we don't have any manifest data, we must assume that all content is - // VOD; |isLive| and |isInProgress| should always return |false|. - it('considers content to be VOD"', async () => { + // TODO: test an HLS live stream on platforms supporting native HLS + it('considers simple mp4 content to be VOD"', async () => { await loadWithSrcEquals(SMALL_MP4_CONTENT_URI); expect(player.isLive()).toBeFalsy(); expect(player.isInProgress()).toBeFalsy(); }); - // Since we don't have any manifest data, we must assume that all content is - // audio-video; |isAudioOnly| should always return |false|. - it('considers content to be audio-video', async () => { + // TODO: test an audio-only mp4 + // TODO: test audio-only HLS on platforms with native HLS + it('considers audio-video mp4 content to be audio-video', async () => { await loadWithSrcEquals(SMALL_MP4_CONTENT_URI); expect(player.isAudioOnly()).toBeFalsy(); }); @@ -122,9 +121,10 @@ describe('Player Src Equals', () => { expect(video.currentTime).toBeLessThan(10.5); }); - // Since we don't have any manifest data, we assume content to be clear. - // This means there should be no key systems, drm info, or expiration time. - it('considers content to be clear ', async () => { + // TODO: test src= with DRM + // TODO: test HLS without DRM on platforms with native HLS + // TODO: test HLS with DRM on platforms with native HLS + it('considers simple content to be clear ', async () => { await loadWithSrcEquals(SMALL_MP4_CONTENT_URI); expect(player.keySystem()).toBe(''); @@ -158,7 +158,7 @@ describe('Player Src Equals', () => { }); // When we load content via src=, can we use the trick play controls to - // control the playback rate? + // control the playback rate. it('can control trick play rate', async () => { await loadWithSrcEquals(SMALL_MP4_CONTENT_URI); @@ -178,108 +178,50 @@ describe('Player Src Equals', () => { expect(video.playbackRate).toBe(1); }); - // Since we don't have a manifest, we can't report what tracks(s) we are - // playing. - it('reports no variant tracks after loading', async () => { + // Since we don't have a manifest, we don't have real variant information. + // TODO: test audio-video mp4 content on platforms with audioTracks API + it('reports no variant tracks for video-only mp4 content', async () => { await loadWithSrcEquals(SMALL_MP4_CONTENT_URI); - expect(player.getVariantTracks()).toEqual([]); + + expect(player.getVariantTracks().length).toBe(0); }); - // Since we are not in-charge of managing variant tracks, we can't select - // tracks. - it('cannot select variant tracks', async () => { + // TODO: test HLS on platforms with native HLS + it('allows selecting variant tracks', async () => { await loadWithSrcEquals(SMALL_MP4_CONTENT_URI); - /** - * Because we can't get tracks from the player, we need to create a fake - * track to ask it to switch to. The player should just ignore this request. - * - * @type {shaka.extern.Track} - * */ - const track = { - active: true, - audioBandwidth: null, - audioCodec: null, - audioId: null, - bandwidth: 123456789, - channelsCount: null, - codecs: null, - frameRate: null, - height: null, - id: 0, - kind: null, - label: null, - language: 'en-US', - mimeType: 'text/mp4', - originalAudioId: null, - originalTextId: null, - originalVideoId: null, - primary: true, - roles: [], - type: 'text', - videoBandwidth: null, - videoCodec: null, - videoId: null, - width: null, - }; - - // This call should be a no-op. WE expect to see no errors throws. - player.selectVariantTrack(track); + // We can only get a variant track here on certain browsers. + const tracks = player.getVariantTracks(); + + // If we have tracks, we should be able to select them. + if (tracks.length) { + // The test fails if this throws. + player.selectVariantTrack(tracks[0]); + } }); - // Since we don't have a manifest, we can't report what tracks(s) we are - // playing. - it('reports no text tracks', async () => { + // TODO: test HLS with text tracks on platforms with native HLS + it('reports no text tracks for simple mp4 content', async () => { await loadWithSrcEquals(SMALL_MP4_CONTENT_URI); expect(player.getTextTracks()).toEqual([]); }); - // Since we don't have a manifest, we can't report what tracks(s) we are - // playing. Even though we can add additional text tracks, since we don't - // initialize any streaming systems, we can't select text tracks. - it('cannot select text tracks', async () => { + // TODO: test HLS on platforms with native HLS + it('allows selecting text tracks', async () => { await loadWithSrcEquals(SMALL_MP4_CONTENT_URI); - /** - * Because we can't get tracks from the player, we need to create a fake - * track to ask it to switch to. The player should just ignore this request. - * - * @type {shaka.extern.Track} - * */ - const track = { - active: true, - audioBandwidth: null, - audioCodec: null, - audioId: null, - bandwidth: 123456789, - channelsCount: null, - codecs: null, - frameRate: null, - height: null, - id: 0, - kind: null, - label: null, - language: 'en-US', - mimeType: 'text/mp4', - originalAudioId: null, - originalTextId: null, - originalVideoId: null, - primary: true, - roles: [], - type: 'text', - videoBandwidth: null, - videoCodec: null, - videoId: null, - width: null, - }; - - // This call should be a no-op. WE expect to see no errors throws. - player.selectTextTrack(track); + // We can only get a text track here on certain browsers. + const tracks = player.getTextTracks(); + + // If we have tracks, we should be able to select them. + if (tracks.length) { + // The test fails if this throws. + player.selectTextTrack(tracks[0]); + } }); - // Since we are not managing the tracks, we can't return any language/role - // for audio or text. - it('returns no languages or roles', async () => { + // TODO: test HLS on platforms with native HLS + it('returns no languages or roles for simple mp4 content', async () => { await loadWithSrcEquals(SMALL_MP4_CONTENT_URI); expect(player.getAudioLanguages()).toEqual([]); @@ -289,9 +231,9 @@ describe('Player Src Equals', () => { expect(player.getTextLanguagesAndRoles()).toEqual([]); }); - // Since we are not managing the tracks, selecting the language/role or audio - // or text should do nothing. - it('cannot select language or role', async () => { + // TODO: test language selection w/ HLS on platforms with native HLS + // This test is disabled until then. + xit('cannot select language or role', async () => { await loadWithSrcEquals(SMALL_MP4_CONTENT_URI); const language = 'en'; @@ -314,7 +256,9 @@ describe('Player Src Equals', () => { expect(player.getTextLanguagesAndRoles()).toEqual([]); }); - it('persists the text visibility setting', async () => { + // TODO: test text visibility w/ HLS on platforms with native HLS + // This test is disabled until then. + xit('persists the text visibility setting', async () => { await loadWithSrcEquals(SMALL_MP4_CONTENT_URI); expect(player.isTextTrackVisible()).toBe(false); diff --git a/test/player_unit.js b/test/player_unit.js index df694948ca..f901f1d898 100644 --- a/test/player_unit.js +++ b/test/player_unit.js @@ -1026,6 +1026,7 @@ describe('Player', function() { videoCodec: 'avc1.4d401f', primary: false, roles: ['main'], + audioRoles: ['main'], videoId: 1, audioId: 3, channelsCount: 6, @@ -1052,6 +1053,7 @@ describe('Player', function() { videoCodec: 'avc1.4d401f', primary: false, roles: ['main'], + audioRoles: ['main'], videoId: 2, audioId: 3, channelsCount: 6, @@ -1078,6 +1080,7 @@ describe('Player', function() { videoCodec: 'avc1.4d401f', primary: false, roles: ['main'], + audioRoles: ['main'], videoId: 1, audioId: 4, channelsCount: 2, @@ -1104,6 +1107,7 @@ describe('Player', function() { videoCodec: 'avc1.4d401f', primary: false, roles: ['main'], + audioRoles: ['main'], videoId: 2, audioId: 4, channelsCount: 2, @@ -1130,6 +1134,7 @@ describe('Player', function() { videoCodec: 'avc1.4d401f', primary: false, roles: ['commentary'], + audioRoles: ['commentary'], videoId: 1, audioId: 5, channelsCount: 2, @@ -1156,6 +1161,7 @@ describe('Player', function() { videoCodec: 'avc1.4d401f', primary: false, roles: ['commentary'], + audioRoles: ['commentary'], videoId: 2, audioId: 5, channelsCount: 2, @@ -1182,6 +1188,7 @@ describe('Player', function() { videoCodec: 'avc1.4d401f', primary: false, roles: [], + audioRoles: [], videoId: 1, audioId: 6, channelsCount: 2, @@ -1208,6 +1215,7 @@ describe('Player', function() { videoCodec: 'avc1.4d401f', primary: false, roles: [], + audioRoles: [], videoId: 2, audioId: 6, channelsCount: 2, @@ -1233,6 +1241,7 @@ describe('Player', function() { videoCodec: null, primary: false, roles: [], + audioRoles: null, channelsCount: null, audioBandwidth: null, videoBandwidth: null, @@ -1259,6 +1268,7 @@ describe('Player', function() { videoCodec: null, primary: false, roles: ['main'], + audioRoles: null, channelsCount: null, audioBandwidth: null, videoBandwidth: null, @@ -1285,6 +1295,7 @@ describe('Player', function() { videoCodec: null, primary: false, roles: ['commentary'], + audioRoles: null, channelsCount: null, audioBandwidth: null, videoBandwidth: null, @@ -2998,7 +3009,7 @@ describe('Player', function() { it('ignores video roles', async () => { manifest = new shaka.test.ManifestGenerator() .addPeriod(0) - .addVariant(0) + .addVariant(0).language('en') .addVideo(1).roles(['video-only-role']) .addAudio(2).roles(['audio-only-role']).language('en') .build();