Skip to content

Commit

Permalink
Support all types of single-file playback
Browse files Browse the repository at this point in the history
Instead of triggering src= based on 'video/mp4' MIME types, ask the
browser what it can support using video.canPlayType().

Querying canPlayType() also allows us to detect src= support for
native HLS in Safari.

The original version of this change also added a complete fallback
system to detect MIME types based on common file extensions and
fetch MIME types via HEAD request when necessary, but the load graph
system does not yet allow us to make async decisions about destination
node.  So this async MIME detection is commented out for now.

Closes #816 (src= single file playback)
Issue #997 (native HLS in Safari)

Change-Id: If1930ca4fd5710481a925d63fb312d9a5b15fec8
  • Loading branch information
joeyparrish committed Apr 16, 2019
1 parent 322876d commit 133f161
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 48 deletions.
95 changes: 69 additions & 26 deletions lib/media/manifest_parser.js
Expand Up @@ -184,30 +184,22 @@ shaka.media.ManifestParser.create = async function(
*/
shaka.media.ManifestParser.getFactory_ = async function(
uri, netEngine, retryParams, mimeType) {
// Try using the mime type we were given.
if (mimeType) {
let mime = mimeType.toLowerCase();
let factory = shaka.media.ManifestParser.parsersByMime[mime];
const ManifestParser = shaka.media.ManifestParser;

// Try using the MIME type we were given.
if (mimeType) {
const factory = ManifestParser.parsersByMime[mimeType.toLowerCase()];
if (factory) {
return factory;
}

shaka.log.warning(
'Could not determine manifest type using mime type ', mime);
'Could not determine manifest type using MIME type ', mimeType);
}

// Try using the uri extension.
let uriObj = new goog.Uri(uri);
let uriPieces = uriObj.getPath().split('/');
let uriFilename = uriPieces.pop();
let filenamePieces = uriFilename.split('.');

// Only one piece means there is no extension.
if (filenamePieces.length > 1) {
let extension = filenamePieces.pop().toLowerCase();
let factory = shaka.media.ManifestParser.parsersByExtension[extension];

const extension = ManifestParser.getExtension(uri);
if (extension) {
const factory = ManifestParser.parsersByExtension[extension];
if (factory) {
return factory;
}
Expand All @@ -218,15 +210,19 @@ shaka.media.ManifestParser.getFactory_ = async function(
shaka.log.warning('Could not find extension for ', uri);
}

let mime = await shaka.media.ManifestParser.getMimeType_(uri,
netEngine,
retryParams);
let factory = shaka.media.ManifestParser.parsersByMime[mime];
if (factory) {
return factory;
}
if (!mimeType) {
mimeType = await ManifestParser.getMimeType(uri, netEngine, retryParams);

shaka.log.warning('Could not determine manifest type using mime type ', mime);
if (mimeType) {
const factory = shaka.media.ManifestParser.parsersByMime[mimeType];
if (factory) {
return factory;
}

shaka.log.warning('Could not determine manifest type using MIME type',
mimeType);
}
}

throw new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
Expand All @@ -241,9 +237,8 @@ shaka.media.ManifestParser.getFactory_ = async function(
* @param {!shaka.net.NetworkingEngine} netEngine
* @param {shaka.extern.RetryParameters} retryParams
* @return {!Promise.<string>}
* @private
*/
shaka.media.ManifestParser.getMimeType_ = async function(
shaka.media.ManifestParser.getMimeType = async function(
uri, netEngine, retryParams) {
const type = shaka.net.NetworkingEngine.RequestType.MANIFEST;

Expand All @@ -257,3 +252,51 @@ shaka.media.ManifestParser.getMimeType_ = async function(
let mimeType = response.headers['content-type'];
return mimeType ? mimeType.toLowerCase() : '';
};


/**
* @param {string} uri
* @return {string}
*/
shaka.media.ManifestParser.getExtension = function(uri) {
const uriObj = new goog.Uri(uri);
const uriPieces = uriObj.getPath().split('/');
const uriFilename = uriPieces.pop();
const filenamePieces = uriFilename.split('.');

// Only one piece means there is no extension.
if (filenamePieces.length == 1) {
return '';
}

return filenamePieces.pop().toLowerCase();
};


/**
* Determines whether or not this URI and MIME type are supported by our own
* manifest parsers on this platform. This takes into account whether or not
* MediaSource is available, as well as which parsers are registered to the
* system.
*
* @param {string} uri
* @param {string} mimeType
* @return {boolean}
*/
shaka.media.ManifestParser.isSupported = function(uri, mimeType) {
// Without MediaSource, our own parsers are useless.
if (!shaka.util.Platform.supportsMediaSource()) {
return false;
}

if (mimeType in shaka.media.ManifestParser.parsersByMime) {
return true;
}

const extension = shaka.media.ManifestParser.getExtension(uri);
if (extension in shaka.media.ManifestParser.parsersByExtension) {
return true;
}

return false;
};
111 changes: 91 additions & 20 deletions lib/player.js
Expand Up @@ -948,10 +948,11 @@ shaka.Player.prototype.load = function(assetUri, startTime, mimeType) {
payload.startTime = startTime;
}

const destination =
this.shouldUseSrcEquals_(payload.uri || '', payload.mimeType || '') ?
this.srcEqualsNode_ :
this.loadNode_;
// TODO: Refactor to determine whether it's a manifest or not, and whether or
// not we can play it. Then we could return a better error than
// UNABLE_TO_GUESS_MANIFEST_TYPE for WebM in Safari.
const useSrcEquals = this.shouldUseSrcEquals_(payload);
const destination = useSrcEquals ? this.srcEqualsNode_ : this.loadNode_;

// Allow this request to be interrupted, this will allow other requests to
// cancel a load and quickly start a new load.
Expand Down Expand Up @@ -993,28 +994,86 @@ shaka.Player.prototype.load = function(assetUri, startTime, mimeType) {
* Check if src= should be used to load the asset at |uri|. Assume that media
* source is the default option, and that src= is for special cases.
*
* @param {string} uri
* @param {string} mimeType
* @param {shaka.routing.Payload} payload
* @return {boolean}
* |true| if the content should be loaded with src=. |false| if the content
* |true| if the content should be loaded with src=, |false| if the content
* should be loaded with MediaSource.
* @private
*/
shaka.Player.prototype.shouldUseSrcEquals_ = function(uri, mimeType) {
shaka.Player.prototype.shouldUseSrcEquals_ = function(payload) {
const Platform = shaka.util.Platform;

// If an explicit ManifestParser factory has been given, we can't do src=.
if (payload.factory) {
return false;
}

// If we are using a platform that does not support media source, we will
// fall back to src= to handle all playback.
if (!shaka.util.Platform.supportsMediaSource()) {
if (!Platform.supportsMediaSource()) {
return true;
}

// The most accurate way to tell the player how to load the content is via
// mime type.
if (mimeType == 'video/mp4' || mimeType == 'audio/mp4') {
return true;
// MIME type. We can fall back to features of the URI if needed.
let mimeType = payload.mimeType;
const uri = payload.uri || '';

// If we don't have a MIME type, try to guess based on the file extension.
// TODO: Too generic to belong to ManifestParser now. Refactor.
if (!mimeType) {
// Try using the uri extension.
const extension = shaka.media.ManifestParser.getExtension(uri);
mimeType = {
'mp4': 'video/mp4',
'm4v': 'video/mp4',
'm4a': 'audio/mp4',
'webm': 'video/webm',
'ts': 'video/mp2t',
'm3u8': 'application/x-mpegurl',
}[extension];
}

// TODO: The load graph system has a design limitation that requires routing
// destination to be chosen synchronously. This means we can only make the
// right choice about src= consistently if we have a well-known file extension
// or API-provided MIME type. Detection of MIME type from a HEAD request (as
// is done for manifest types) can't be done yet.

if (mimeType) {
// If we have a MIME type, check if the browser can play it natively.
// This will cover both single files and native HLS.
const canPlayNatively = Platform.supportsMediaType(mimeType);

// If we can't play natively, then src= isn't an option.
if (!canPlayNatively) {
return false;
}

const canPlayMediaSource =
shaka.media.ManifestParser.isSupported(uri, mimeType);

// If MediaSource isn't an option, the native option is our only chance.
if (!canPlayMediaSource) {
return true;
}

// If we land here, both are feasible.
goog.asserts.assert(canPlayNatively && canPlayMediaSource,
'Both native and MSE playback should be possible!');

// We would prefer MediaSource in some cases, and src= in others. For
// example, Android has native HLS, but we'd prefer our own MediaSource
// version there. But for Safari desktop, we'd prefer the native one for
// now, because that's the only way we get FairPlay there. So use src= over
// MSE on any Apple platform.
return Platform.isApple();
}

// Our last check is always the file extension.
return uri.endsWith('.mp4');
// Unless there are good reasons to use src= (single-file playback or native
// HLS), we prefer MediaSource. So the final return value for choosing src=
// is false.
return false;
};

/**
Expand Down Expand Up @@ -1718,6 +1777,7 @@ shaka.Player.prototype.onSrcEquals_ = function(has, wants) {
//
// TODO: If 2 seconds appears to be a poor choice, explore different options
// for what we can do to detect/track buffering with src=.
// TODO: This is broken for native HLS on Safari.
this.startBufferManagement_(/* rebuffer= */ 2);

// Add all media element listeners.
Expand Down Expand Up @@ -2352,8 +2412,7 @@ shaka.Player.prototype.isInProgress = function() {
* @export
*/
shaka.Player.prototype.isAudioOnly = function() {
// TODO: When the audio tracks api on HTMLMediaElement is generally supported
// we should be able to check if content is audio only.
// TODO: Safari's native HLS has audioTracks/videoTracks on HTMLMediaElement.

if (this.loadMode_ != shaka.Player.LoadMode.MEDIA_SOURCE) {
return false;
Expand Down Expand Up @@ -2543,8 +2602,7 @@ shaka.Player.prototype.cancelTrickPlay = function() {
* @export
*/
shaka.Player.prototype.getVariantTracks = function() {
// TODO: Once we get consistent behaviour from the tracks api across browsers
// we should be able to provide variant track information for src=.
// TODO: Safari's native HLS has audioTracks/videoTracks on HTMLMediaElement.

if (this.loadMode_ != shaka.Player.LoadMode.MEDIA_SOURCE) {
return [];
Expand Down Expand Up @@ -2578,8 +2636,7 @@ shaka.Player.prototype.getVariantTracks = function() {
* @export
*/
shaka.Player.prototype.getTextTracks = function() {
// TODO: Once we get consistent behaviour from the tracks api across browsers
// we should be able to provide variant track information for src=.
// TODO: Safari's native HLS has textTracks on HTMLMediaElement.

if (this.loadMode_ != shaka.Player.LoadMode.MEDIA_SOURCE) {
return [];
Expand Down Expand Up @@ -2773,6 +2830,7 @@ shaka.Player.prototype.getAudioLanguagesAndRoles = function() {
// 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: Safari's native HLS has audioTracks on HTMLMediaElement.

if (this.loadMode_ != shaka.Player.LoadMode.MEDIA_SOURCE) {
return [];
Expand All @@ -2797,6 +2855,8 @@ shaka.Player.prototype.getAudioLanguagesAndRoles = function() {
* @export
*/
shaka.Player.prototype.getTextLanguagesAndRoles = function() {
// TODO: Safari's native HLS has textTracks on HTMLMediaElement.

if (this.loadMode_ != shaka.Player.LoadMode.MEDIA_SOURCE) {
return [];
}
Expand All @@ -2816,6 +2876,7 @@ shaka.Player.prototype.getTextLanguagesAndRoles = function() {
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: Safari's native HLS has audioTracks on HTMLMediaElement.

if (this.loadMode_ != shaka.Player.LoadMode.MEDIA_SOURCE) {
return [];
Expand All @@ -2840,6 +2901,8 @@ shaka.Player.prototype.getAudioLanguages = function() {
* @export
*/
shaka.Player.prototype.getTextLanguages = function() {
// TODO: Safari's native HLS has textTracks on HTMLMediaElement.

if (this.loadMode_ != shaka.Player.LoadMode.MEDIA_SOURCE) {
return [];
}
Expand All @@ -2858,6 +2921,8 @@ shaka.Player.prototype.getTextLanguages = function() {
* @export
*/
shaka.Player.prototype.selectAudioLanguage = function(language, role) {
// TODO: Safari's native HLS has audioTracks on HTMLMediaElement.

if (this.loadMode_ != shaka.Player.LoadMode.MEDIA_SOURCE) {
return;
}
Expand All @@ -2883,6 +2948,8 @@ shaka.Player.prototype.selectAudioLanguage = function(language, role) {
* @export
*/
shaka.Player.prototype.selectTextLanguage = function(language, role) {
// TODO: Safari's native HLS has textTracks on HTMLMediaElement.

if (this.loadMode_ != shaka.Player.LoadMode.MEDIA_SOURCE) {
return;
}
Expand All @@ -2907,6 +2974,8 @@ shaka.Player.prototype.selectTextLanguage = function(language, role) {
* @export
*/
shaka.Player.prototype.isTextTrackVisible = function() {
// TODO: Safari's native HLS has textTracks on HTMLMediaElement.

// Right now we don't support text with src=. So the only time that text could
// be visible is when we have loaded content with media source.
if (this.loadMode_ != shaka.Player.LoadMode.MEDIA_SOURCE) {
Expand Down Expand Up @@ -2936,6 +3005,8 @@ shaka.Player.prototype.isTextTrackVisible = function() {
* @export
*/
shaka.Player.prototype.setTextTrackVisibility = async function(isVisible) {
// TODO: Safari's native HLS has textTracks on HTMLMediaElement.

const oldVisibilty = this.isTextVisible_;
const newVisibility = isVisible;

Expand Down
2 changes: 1 addition & 1 deletion lib/polyfill/mediasource.js
Expand Up @@ -48,7 +48,7 @@ shaka.polyfill.MediaSource.install = function() {
shaka.log.info('Patching Chromecast MSE bugs.');
// Chromecast cannot make accurate determinations via isTypeSupported.
shaka.polyfill.MediaSource.patchCastIsTypeSupported_();
} else if (navigator.vendor && navigator.vendor.includes('Apple')) {
} else if (shaka.util.Platform.isApple()) {
let version = navigator.appVersion;

// TS content is broken on Safari in general.
Expand Down
9 changes: 9 additions & 0 deletions lib/util/platform.js
Expand Up @@ -126,6 +126,15 @@ shaka.util.Platform = class {
!shaka.util.Platform.isEdge();
}

/**
* Check if the current platform is an Apple device (iOS, desktop Safari, etc)
*
* @return {boolean}
*/
static isApple() {
return !!navigator.vendor && navigator.vendor.includes('Apple');
}

/**
* Check if the user agent contains a key. This is the best way we know of
* right now to detect platforms. If there is a better way, please send a
Expand Down
2 changes: 1 addition & 1 deletion test/player_integration.js
Expand Up @@ -840,7 +840,7 @@ describe('Player Load Path', () => {
await load2;
});

it('unload will interupt load', async () => {
it('unload will interrupt load', async () => {
createPlayer(/* attachedTo= */ null);

await player.attach(video);
Expand Down

0 comments on commit 133f161

Please sign in to comment.