mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-27 17:56:23 +03:00
c7368024ae
I went ahead and implemented the full structured preference system that was discussed in https://github.com/shaka-project/shaka-player/issues/1591. Instead of just expanding languages to arrays, I replaced all 14 individual preference fields with 3 structured arrays: ```tsx preferredAudio (language, role, label, channelCount, codec, spatialAudio) preferredText (language, role, format, forced) preferredVideo (label, role, codec, hdrLevel, layout) ``` Each array entry works as an AND filter - so you can say things like "I want Korean with 5.1 surround, but if not available, English is fine too": ```tsx player.configure('preferredAudio', [ {language: 'ko', channelCount: 6}, {language: 'ko'}, {language: 'en'}, ]); ``` <img width="1728" height="965" alt="image" src="https://github.com/user-attachments/assets/7b088150-139b-475e-bdba-5bc77dd4e524" /> **Config** - Replaced the 14 individual fields with 3 arrays of typed preference objects (AudioPreference, TextPreference, VideoPreference). The old fields still work at runtime with a deprecation warning, so existing apps won't break immediately. **Demo** - The demo config UI now shows inline expandable preference lists instead of flat text inputs. You can add/remove entries and configure each field per entry. URL hash serialization was updated to use JSON format, with legacy param fallbacks preserved.
245 lines
7.9 KiB
JavaScript
245 lines
7.9 KiB
JavaScript
/*! @license
|
|
* Shaka Player
|
|
* Copyright 2016 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
describe('HlsParser', () => {
|
|
const Util = shaka.test.Util;
|
|
|
|
/** @type {!jasmine.Spy} */
|
|
let onErrorSpy;
|
|
|
|
/** @type {!HTMLVideoElement} */
|
|
let video;
|
|
/** @type {shaka.Player} */
|
|
let player;
|
|
/** @type {!shaka.util.EventManager} */
|
|
let eventManager;
|
|
|
|
let compiledShaka;
|
|
|
|
/** @type {!shaka.test.Waiter} */
|
|
let waiter;
|
|
|
|
beforeAll(async () => {
|
|
video = shaka.test.UiUtils.createVideoElement();
|
|
document.body.appendChild(video);
|
|
compiledShaka =
|
|
await shaka.test.Loader.loadShaka(getClientArg('uncompiled'));
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
await shaka.test.TestScheme.createManifests(compiledShaka, '_compiled');
|
|
player = new compiledShaka.Player();
|
|
await player.attach(video);
|
|
|
|
// Disable stall detection, which can interfere with playback tests.
|
|
player.configure('streaming.stallEnabled', false);
|
|
// Disable gapPadding, which can interfere with playback tests.
|
|
player.configure('streaming.gapPadding', 0);
|
|
|
|
// Grab event manager from the uncompiled library:
|
|
eventManager = new shaka.util.EventManager();
|
|
waiter = new shaka.test.Waiter(eventManager);
|
|
waiter.setPlayer(player);
|
|
|
|
onErrorSpy = jasmine.createSpy('onError');
|
|
onErrorSpy.and.callFake((event) => fail(event.detail));
|
|
eventManager.listen(player, 'error', Util.spyFunc(onErrorSpy));
|
|
});
|
|
|
|
afterEach(async () => {
|
|
eventManager.release();
|
|
await player.destroy();
|
|
});
|
|
|
|
afterAll(() => {
|
|
document.body.removeChild(video);
|
|
});
|
|
|
|
it('supports AES-256 streaming', async () => {
|
|
let keyRequests = 0;
|
|
const netEngine = player.getNetworkingEngine();
|
|
netEngine.registerRequestFilter((type, request, context) => {
|
|
if (type == shaka.net.NetworkingEngine.RequestType.KEY) {
|
|
keyRequests++;
|
|
}
|
|
});
|
|
await player.load('/base/test/test/assets/hls-aes-256/media.m3u8');
|
|
await video.play();
|
|
expect(player.isLive()).toBe(false);
|
|
|
|
// Wait for the video to start playback. If it takes longer than 10
|
|
// seconds, fail the test.
|
|
await waiter.waitForMovementOrFailOnTimeout(video, 10);
|
|
|
|
// Play for 8 seconds, but stop early if the video ends. If it takes
|
|
// longer than 30 seconds, fail the test.
|
|
await waiter.waitUntilPlayheadReachesOrFailOnTimeout(video, 8, 30);
|
|
|
|
await player.unload();
|
|
|
|
// The stream has 6 #EXT-X-KEY but only 5 different keys.
|
|
expect(keyRequests).toBe(5);
|
|
});
|
|
|
|
drmIt('supports SAMPLE-AES identity streaming', async () => {
|
|
if (!checkClearKeySupport()) {
|
|
pending('ClearKey is not supported');
|
|
}
|
|
|
|
await player.load('/base/test/test/assets/hls-sample-aes/index.m3u8');
|
|
await video.play();
|
|
expect(player.isLive()).toBe(false);
|
|
|
|
// Wait for the video to start playback. If it takes longer than 10
|
|
// seconds, fail the test.
|
|
await waiter.waitForMovementOrFailOnTimeout(video, 10);
|
|
|
|
// Play for 8 seconds, but stop early if the video ends. If it takes
|
|
// longer than 30 seconds, fail the test.
|
|
await waiter.waitUntilPlayheadReachesOrFailOnTimeout(video, 8, 30);
|
|
|
|
await player.unload();
|
|
});
|
|
|
|
it('supports text discontinuity', async () => {
|
|
player.configure('preferredText',
|
|
[{
|
|
language: 'en',
|
|
role: '',
|
|
format: '',
|
|
forced: false,
|
|
}]);
|
|
await player.load('/base/test/test/assets/hls-text-offset/index.m3u8');
|
|
await video.play();
|
|
|
|
// Wait for last cue
|
|
await waiter.waitUntilPlayheadReachesOrFailOnTimeout(video, 8, 30);
|
|
|
|
const cues = video.textTracks[0].cues;
|
|
expect(cues.length).toBe(3);
|
|
expect(cues[0].startTime).toBeCloseTo(0, 0);
|
|
expect(cues[0].endTime).toBeCloseTo(2, 0);
|
|
expect(cues[1].startTime).toBeCloseTo(2, 0);
|
|
expect(cues[1].endTime).toBeCloseTo(4, 0);
|
|
expect(cues[2].startTime).toBeCloseTo(6, 0);
|
|
expect(cues[2].endTime).toBeCloseTo(8, 0);
|
|
|
|
await player.unload();
|
|
});
|
|
|
|
it('supports text without discontinuity', async () => {
|
|
player.configure('preferredText',
|
|
[{
|
|
language: 'de',
|
|
role: '',
|
|
format: '',
|
|
forced: false,
|
|
}]);
|
|
// eslint-disable-next-line @stylistic/max-len
|
|
await player.load('/base/test/test/assets/hls-text-no-discontinuity/index.m3u8');
|
|
await video.play();
|
|
|
|
// This test sometimes fails on Tizen with missing cues if we use too
|
|
// small a delay here.
|
|
await waiter.waitUntilPlayheadReachesOrFailOnTimeout(video, 3, 30);
|
|
|
|
const cues = video.textTracks[0].cues;
|
|
expect(cues.length).toBe(3);
|
|
expect(cues[0].startTime).toBeCloseTo(0, 0);
|
|
expect(cues[0].endTime).toBeCloseTo(1.48, 0);
|
|
expect(cues[1].startTime).toBeCloseTo(1.48, 0);
|
|
expect(cues[1].endTime).toBeCloseTo(4.96, 0);
|
|
expect(cues[2].startTime).toBeCloseTo(4.96, 0);
|
|
expect(cues[2].endTime).toBeCloseTo(9.28, 0);
|
|
|
|
await player.unload();
|
|
});
|
|
|
|
it('allow switch between mp4 muxed and ts muxed', async () => {
|
|
if (!await Util.isTypeSupported(
|
|
'video/mp4; codecs="av01.0.31M.08"',
|
|
/* width= */ 1920, /* height= */ 1080)) {
|
|
pending('Codec AV1 is not supported by the platform.');
|
|
}
|
|
player.configure('abr.enabled', false);
|
|
await player.load('/base/test/test/assets/hls-muxed-mp4-ts/master.m3u8');
|
|
await video.play();
|
|
|
|
expect(player.getVariantTracks().length).toBe(2);
|
|
|
|
// We want to test TS --> MP4 and MP4 --> TS, that's why
|
|
// selectVariantTrack is called twice
|
|
|
|
await waiter.waitUntilPlayheadReachesOrFailOnTimeout(video, 1, 30);
|
|
|
|
let nonActiveVariant = player.getVariantTracks().find((v) => !v.active);
|
|
goog.asserts.assert(nonActiveVariant, 'variant should be non-null!');
|
|
player.selectVariantTrack(nonActiveVariant, /* clearBuffer= */ true);
|
|
|
|
await waiter.waitUntilPlayheadReachesOrFailOnTimeout(video, 3, 30);
|
|
|
|
nonActiveVariant = player.getVariantTracks().find((v) => !v.active);
|
|
goog.asserts.assert(nonActiveVariant, 'variant should be non-null!');
|
|
player.selectVariantTrack(nonActiveVariant, /* clearBuffer= */ true);
|
|
|
|
await waiter.waitUntilPlayheadReachesOrFailOnTimeout(video, 5, 30);
|
|
|
|
await player.unload();
|
|
});
|
|
|
|
it('supports com.apple.hls.chapters', async () => {
|
|
await player.load('/base/test/test/assets/hls-chapters/index.m3u8');
|
|
await video.play();
|
|
expect(player.isLive()).toBe(false);
|
|
|
|
expect(player.getChaptersTracks().length).toBe(1);
|
|
|
|
const chapters = await player.getChaptersAsync('und');
|
|
|
|
expect(chapters.length).toBe(7);
|
|
|
|
await player.unload();
|
|
});
|
|
|
|
it('supports mp4 muxed with AAC and H.264', async () => {
|
|
if (deviceDetected.getDeviceName() === 'Tizen' &&
|
|
deviceDetected.getVersion() === 3) {
|
|
pending('Tizen 3 currently does not support mp4 muxed content');
|
|
}
|
|
await player.load('/base/test/test/assets/hls-mp4-muxed-aac-h264/hls.m3u8');
|
|
await video.play();
|
|
expect(player.isLive()).toBe(false);
|
|
|
|
// Wait for the video to start playback. If it takes longer than 10
|
|
// seconds, fail the test.
|
|
await waiter.waitForMovementOrFailOnTimeout(video, 10);
|
|
|
|
// Play for 8 seconds, but stop early if the video ends. If it takes
|
|
// longer than 30 seconds, fail the test.
|
|
await waiter.waitUntilPlayheadReachesOrFailOnTimeout(video, 8, 30);
|
|
|
|
await player.unload();
|
|
});
|
|
|
|
it('supports playback with gaps', async () => {
|
|
await player.load('/base/test/test/assets/hls-gap/playlist.m3u8');
|
|
await video.play();
|
|
expect(player.isLive()).toBe(false);
|
|
|
|
expect(player.getStats().manifestGapCount).toBe(2);
|
|
|
|
// Wait for the video to start playback. If it takes longer than 10
|
|
// seconds, fail the test.
|
|
await waiter.waitForMovementOrFailOnTimeout(video, 10);
|
|
|
|
// Play for 15 seconds, but stop early if the video ends. If it takes
|
|
// longer than 45 seconds, fail the test.
|
|
await waiter.waitUntilPlayheadReachesOrFailOnTimeout(video, 15, 45);
|
|
|
|
await player.unload();
|
|
});
|
|
});
|