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
18 changes: 10 additions & 8 deletions src/streaming/controllers/AlternativeMediaController.js
Original file line number Diff line number Diff line change
Expand Up @@ -268,17 +268,19 @@ function AlternativeMediaController() {

const altPlayer = mediaManager.getAlternativePlayer();

const deltaTime = e.time - timeToSwitch;
if (!alternativeSwitched) {
const adjustedTime = e.time - timeToSwitch;
if (!alternativeSwitched && adjustedTime > 0) {
alternativeSwitched = true;
calculatedMaxDuration = altPlayer.isDynamic() ? deltaTime + maxDuration : maxDuration;
calculatedMaxDuration = altPlayer.isDynamic() ? adjustedTime + maxDuration : maxDuration;
}
const shouldSwitchBack =
// Check if the alternative content has finished playing
(Math.round(altPlayer.duration() - e.time) === 0) ||
// Check if the alternative content reached the max duration
(clip && actualEventPresentationTime + deltaTime >= presentationTime + calculatedMaxDuration) ||
(calculatedMaxDuration && calculatedMaxDuration <= e.time);
calculatedMaxDuration > 0 && (
// Check if the alternative content has finished playing (only for non-dynamic content)
(!altPlayer.isDynamic() && Math.round(altPlayer.duration() - e.time) === 0) ||
// Check if the alternative content reached the max duration
(clip && actualEventPresentationTime + adjustedTime >= presentationTime + calculatedMaxDuration) ||
(calculatedMaxDuration && calculatedMaxDuration <= adjustedTime)
);
if (shouldSwitchBack) {
const seekTime = _calculateSeekTime(event, altPlayer);
mediaManager.switchBackToMainContent(seekTime);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
"feature-support/alternative/alternative-mpd-replace-vod",
"feature-support/alternative/alternative-mpd-insert-vod",
"feature-support/alternative/alternative-mpd-replace-live",
"feature-support/alternative/alternative-mpd-executeOnce"
"feature-support/alternative/alternative-mpd-executeOnce",
"feature-support/alternative/alternative-mpd-clip-vod",
"feature-support/alternative/alternative-mpd-clip-live"
],
"excluded": []
},
Expand Down Expand Up @@ -79,6 +81,23 @@
"includedTestfiles": [
"feature-support/alternative/alternative-mpd-executeOnce"
]
},
{
"name": "Alternative MPD Clip - VOD to VOD Test",
"type": "vod",
"url": "/base/test/functional/content/alternative-mpd/alternative-mpd-clip.mpd",
"includedTestfiles": [
"feature-support/alternative/alternative-mpd-clip-vod"
]
},
{
"name": "Alternative MPD Clip - Live to Live Test",
"type": "live",
"originalUrl": "https://livesim2.dashif.org/livesim2/testpic_2s/Manifest.mpd",
"alternativeUrl": "https://livesim2.dashif.org/livesim2/testpic_2s/Manifest.mpd",
"includedTestfiles": [
"feature-support/alternative/alternative-mpd-clip-live"
]
}
]
}
22 changes: 22 additions & 0 deletions test/functional/content/alternative-mpd/alternative-mpd-clip.mpd
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<MPD xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="urn:mpeg:dash:schema:mpd:2011" xmlns:xlink="http://www.w3.org/1999/xlink" xsi:schemaLocation="urn:mpeg:DASH:schema:MPD:2011 http://standards.iso.org/ittf/PubliclyAvailableStandards/MPEG-DASH_schema_files/DASH-MPD.xsd" profiles="urn:mpeg:dash:profile:isoff-live:2011" type="static" mediaPresentationDuration="PT1M00s">
<BaseURL>https://dash.akamaized.net/akamai/bbb_30fps/</BaseURL>
<Period id="0" start="PT0.0S">
<EventStream xmlns="" schemeIdUri="urn:mpeg:dash:event:alternativeMPD:replace:2025" timescale="1000">
<Event presentationTime="5000" duration="9000" id="1">
<ReplacePresentation url="https://dash.akamaized.net/dashif/ad-insertion-testcase1/batch2/real/b/ad-insertion-testcase1.mpd" earliestResolutionTimeOffset="5000" maxDuration="9000" clip="true"/>
</Event>
</EventStream>
<AdaptationSet mimeType="video/mp4" contentType="video" subsegmentAlignment="true" subsegmentStartsWithSAP="1" par="16:9">
<SegmentTemplate duration="120" timescale="30" media="$RepresentationID$/$RepresentationID$_$Number$.m4v" startNumber="1" initialization="$RepresentationID$/$RepresentationID$_0.m4v"/>
<Representation id="bbb_30fps_1280x720_4000k" codecs="avc1.64001f" bandwidth="4952892" width="1280" height="720" frameRate="30" sar="1:1" scanType="progressive"/>
</AdaptationSet>
<AdaptationSet mimeType="audio/mp4" contentType="audio" subsegmentAlignment="true" subsegmentStartsWithSAP="1">
<Accessibility schemeIdUri="urn:tva:metadata:cs:AudioPurposeCS:2007" value="6"/>
<Role schemeIdUri="urn:mpeg:dash:role:2011" value="main"/>
<SegmentTemplate duration="192512" timescale="48000" media="$RepresentationID$/$RepresentationID$_$Number$.m4a" startNumber="1" initialization="$RepresentationID$/$RepresentationID$_0.m4a"/>
<Representation id="bbb_a64k" codecs="mp4a.40.5" bandwidth="67071" audioSamplingRate="48000">
<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"/>
</Representation>
</AdaptationSet>
</Period>
</MPD>
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import Constants from '../../../../../src/streaming/constants/Constants.js';
import Utils from '../../../src/Utils.js';
import { initializeDashJsAdapterForAlternativMedia } from '../../common/common.js';
import { expect } from 'chai';

/**
* Utility function to modify a live manifest by injecting Alternative MPD events with clip functionality
* This tests the clip feature scenarios where only a portion of the alternative live content is played
*/
function injectAlternativeMpdClipEvents(player, originalManifestUrl, alternativeManifestUrl, presentationTime, maxDuration, callback) {
const mediaPlayer = player.player;

mediaPlayer.retrieveManifest(originalManifestUrl, (manifest) => {
if (!manifest.Period[0].EventStream) {
manifest.Period[0].EventStream = [];
} else {
manifest.Period[0].EventStream = [];
}

const duration = 8000;
const earliestResolutionTimeOffset = 3000;

const replaceClipEvent = {
schemeIdUri: 'urn:mpeg:dash:event:alternativeMPD:replace:2025',
timescale: 1000,
Event: [{
id: 1,
presentationTime: presentationTime,
duration: duration,
ReplacePresentation: {
url: alternativeManifestUrl,
earliestResolutionTimeOffset: earliestResolutionTimeOffset,
maxDuration: maxDuration,
clip: 'true',
}
}]
};

manifest.Period[0].EventStream.push(replaceClipEvent);
mediaPlayer.attachSource(manifest);

if (callback) {
callback();
}
});
}

Utils.getTestvectorsForTestcase('feature-support/alternative/alternative-mpd-clip-live').forEach((item) => {
const name = item.name;
const originalUrl = item.originalUrl;
const alternativeUrl = item.alternativeUrl;

describe(`Alternative MPD Replace with Clip functionality tests for Live-to-Live: ${name}`, () => {

let player;
let presentationTime;
let maxDuration;
let presentationTimeOffset;

before((done) => {
const currentPresentationTime = Date.now();
presentationTimeOffset = 10000 //includes potential latency
presentationTime = currentPresentationTime - presentationTimeOffset; //alternative content already started
maxDuration = 10000;

// Initialize the player without attaching source immediately
player = initializeDashJsAdapterForAlternativMedia(item, null);

// Use the utility function to inject Alternative MPD events with clip for live-to-live
injectAlternativeMpdClipEvents(player, originalUrl, alternativeUrl, presentationTime, maxDuration, () => {
done();
});
});

after(() => {
if (player) {
player.destroy();
}
});

it('should play live content, switch to clipped alternative live content, then back to original live content', (done) => {
let alternativeContentDetected = false;
let backToOriginalDetected = false;
let eventTriggered = false;
let alternativeEndTime = 0;
let alternativeStartTime = 0;
let expectedMaxDuration = 0;
let expectedPresentationTime = 0;

const timeout = setTimeout(() => {
done(new Error('Test timed out - alternative MPD replace clip event not completed within 35 seconds'));
}, 35000);

player.registerEvent(Constants.ALTERNATIVE_MPD.URIS.REPLACE, () => {
eventTriggered = true;
});

player.registerEvent(Constants.ALTERNATIVE_MPD.CONTENT_START, (data) => {
if (data.event.mode === 'replace') {
alternativeContentDetected = true;
alternativeStartTime = Date.now() / 1000;
expectedMaxDuration = data.event.maxDuration;
expectedPresentationTime = data.event.presentationTime;
expect(data.event.clip).to.be.true;
}
});

player.registerEvent(Constants.ALTERNATIVE_MPD.CONTENT_END, (data) => {
if (data.event.mode === 'replace') {
backToOriginalDetected = true;
alternativeEndTime = Date.now() / 1000;
const actualTerminationTime = player.player.timeAsUTC();
const expectedTerminationTime = (expectedPresentationTime + expectedMaxDuration);
clearTimeout(timeout);

// Wait to ensure stability
setTimeout(() => {
expect(eventTriggered).to.be.true;
expect(alternativeContentDetected).to.be.true;
expect(backToOriginalDetected).to.be.true;

// Verify that the actual duration of alternative content is less than maxDuration
const actualAlternativeDuration = (alternativeEndTime - alternativeStartTime);
expect(actualAlternativeDuration).to.be.lessThan(expectedMaxDuration);

// The alternative content should terminate at approximately PRT + APDmax
// Allow tolerance for live content timing variations
expect(actualTerminationTime).to.be.at.closeTo(expectedTerminationTime, 2);

done();
}, 2000); // Longer wait for live content stability
}
});

// Handle errors
player.registerEvent('error', (e) => {
clearTimeout(timeout);
done(new Error(`Player error: ${JSON.stringify(e)}`));
});

}, 45000);

});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import Constants from '../../../../../src/streaming/constants/Constants.js';
import Utils from '../../../src/Utils.js';
import { initializeDashJsAdapterForAlternativMedia } from '../../common/common.js';
import { expect } from 'chai';

Utils.getTestvectorsForTestcase('feature-support/alternative/alternative-mpd-clip-vod').forEach((item) => {
const name = item.name;
const url = item.url;

describe(`Alternative MPD Clip functionality tests for VOD-to-VOD: ${name}`, () => {

let player;

before(() => {
player = initializeDashJsAdapterForAlternativMedia(item, url);
});

after(() => {
if (player) {
player.destroy();
}
});

it('should play VOD content, seek forward to simulate delay, then test clip behavior with alternative VOD content', (done) => {
let alternativeContentDetected = false;
let backToOriginalDetected = false;
let eventTriggered = false;
let alternativeEndTime = 0;
let expectedMaxDuration = 0;
let expectedPresentationTime = 0;
let seekPerformed = false;

const timeout = setTimeout(() => {
done(new Error('Test timed out - alternative MPD replace clip event not completed within 30 seconds'));
}, 30000);

player.registerEvent(Constants.ALTERNATIVE_MPD.URIS.REPLACE, () => {
eventTriggered = true;
});

player.registerEvent(Constants.ALTERNATIVE_MPD.CONTENT_START, (data) => {
if (data.event.mode === 'replace') {
alternativeContentDetected = true;
expectedMaxDuration = data.event.maxDuration;
expectedPresentationTime = data.event.presentationTime;

expect(data.event.clip).to.be.true;
}
});

player.registerEvent(Constants.ALTERNATIVE_MPD.CONTENT_END, (data) => {
if (data.event.mode === 'replace') {
alternativeEndTime = player.getCurrentTime();
backToOriginalDetected = true;
clearTimeout(timeout);

// Wait to ensure stability
setTimeout(() => {
expect(eventTriggered).to.be.true;
expect(alternativeContentDetected).to.be.true;
expect(backToOriginalDetected).to.be.true;

// With clip="true", alternative should terminate at PRT + maxDuration
const expectedTerminationTime = expectedPresentationTime + expectedMaxDuration;
expect(alternativeEndTime).to.be.closeTo(expectedTerminationTime, 0.5);
done();
}, 1000); // Wait for VOD content stability
}
});

// Perform seek forward to simulate delay after player starts
player.registerEvent('playbackStarted', () => {
if (!seekPerformed) {
seekPerformed = true;

// Wait a moment for stable playback, then seek forward
setTimeout(() => {
const seekTime = 7; // Seek to 7 seconds - event should have started at 5s
player.seek(seekTime);
}, 2000);
}
});

// Handle errors
player.registerEvent('error', (e) => {
clearTimeout(timeout);
done(new Error(`Player error: ${JSON.stringify(e)}`));
});

}, 35000);

});
});
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ Utils.getTestvectorsForTestcase('feature-support/alternative/alternative-mpd-ins

// Handle errors
player.registerEvent('error', (e) => {
console.error('Player error:', e);
clearTimeout(timeout);
done(new Error(`Player error: ${JSON.stringify(e)}`));
});

}, 35000);
Expand Down
Loading