Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 33 additions & 10 deletions packages/playback-core/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,17 +59,13 @@ export const toPlaybackIdParts = (playbackIdWithOptionalParams: string): [string
return [idPart, queryPart];
};

export const getType = (props: Partial<Pick<MuxMediaProps, 'type' | 'src'>>) => {
const type = props.type;
export const getType = (props: Partial<Pick<MuxMediaProps, 'type' | 'src' | 'customDomain'>>) => {
const { type } = props;
if (type) {
const upperType = type.toUpperCase();
return isKeyOf(upperType, MimeTypeShorthandMap) ? MimeTypeShorthandMap[upperType] : type;
}

const { src } = props;
if (!src) return '';

return inferMimeTypeFromURL(src);
return inferMimeTypeFromURL(props);
};

export const toStreamTypeFromPlaylistType = (playlistType: HlsPlaylistTypes) => {
Expand All @@ -82,23 +78,50 @@ export const toTargetLiveWindowFromPlaylistType = (playlistType: HlsPlaylistType
return 0;
};

export const inferMimeTypeFromURL = (url: string) => {
export const inferMimeTypeFromURL = (props: Partial<Pick<MuxMediaProps, 'src' | 'customDomain'>>) => {
const { src } = props;
if (!src) return '';

let pathname = '';
try {
pathname = new URL(url).pathname;
pathname = new URL(src).pathname;
} catch (_e) {
console.error('invalid url');
}

const extDelimIdx = pathname.lastIndexOf('.');
if (extDelimIdx < 0) return '';
if (extDelimIdx < 0) {
if (isExtensionLessMuxM3U8URL(props)) {
return ExtensionMimeTypeMap.M3U8; // Treat extension-less Mux URLs as HLS
}
return '';
}

const ext = pathname.slice(extDelimIdx + 1);
const upperExt = ext.toUpperCase();

return isKeyOf(upperExt, ExtensionMimeTypeMap) ? ExtensionMimeTypeMap[upperExt] : '';
};

const MUX_VIDEO_DOMAIN = 'mux.com';
export const isExtensionLessMuxM3U8URL = ({
src,
customDomain = MUX_VIDEO_DOMAIN,
}: Partial<Pick<MuxMediaProps, 'src' | 'customDomain'>>) => {
let urlObj;
try {
urlObj = new URL(`${src}`);
} catch {
return false;
}
const validProtocol = urlObj.protocol === 'https:';
const validHostname = urlObj.hostname === `stream.${customDomain}`.toLowerCase();
const pathParts = urlObj.pathname.split('/');
const validPathPartsLength = pathParts.length === 2;
const validExtensionlessPath = !pathParts?.[1].includes('.');
return validProtocol && validHostname && validPathPartsLength && validExtensionlessPath;
};

export type MuxJWT = {
sub: string;
aud: 'v' | 't' | 'g' | 's' | 'd';
Expand Down
14 changes: 14 additions & 0 deletions packages/playback-core/test/util.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,20 @@ describe('Module: util', () => {
const actual = getType({ src: `http://foo.com/bar.${extension}`, type: MimeTypeShorthandMap.HLS });
assert.equal(actual, expected);
});

it('should support Mux extensionless m3u8 URLs', () => {
const expected = ExtensionMimeTypeMap.M3U8;
// Mux extensionless m3u8 URLs should be treated as M3U8
const actual = getType({ src: 'https://stream.mux.com/abc123' });
assert.equal(actual, expected);
});

it('should support Mux extensionless m3u8 URLs with custom domain', () => {
const expected = ExtensionMimeTypeMap.M3U8;
// Mux extensionless m3u8 URLs with custom domain should be treated as M3U8
const actual = getType({ src: 'https://stream.abc.com/abc123', customDomain: 'abc.com' });
assert.equal(actual, expected);
});
});

describe('parseJwt', () => {
Expand Down
Loading