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
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"feature-support/alternative/alternative-mpd-executeOnce",
"feature-support/alternative/alternative-mpd-clip-vod",
"feature-support/alternative/alternative-mpd-clip-live",
"feature-support/alternative/alternative-mpd-status-update-live",
"feature-support/alternative/alternative-mpd-returnOffset"
],
"excluded": []
Expand Down Expand Up @@ -100,6 +101,15 @@
"feature-support/alternative/alternative-mpd-clip-live"
]
},
{
"name": "Alternative MPD Status Update - 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-status-update-live"
]
},
{
"name": "Alternative MPD returnOffset - Live to Live Test",
"type": "live",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
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 without maxDuration
* This simulates the initial event that starts alternative content playback without a preset duration limit
*/
function injectInitialAlternativeMpdEvent(player, originalManifestUrl, alternativeManifestUrl, presentationTime, callback) {
const mediaPlayer = player.player;

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

const duration = 15000; // 15 seconds default duration
const earliestResolutionTimeOffset = 3000;
Comment on lines +16 to +17

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The values for duration and earliestResolutionTimeOffset are hardcoded here and also in injectStatusUpdateEvent (lines 61 and 63). To improve maintainability and avoid duplication, please define them as constants at a higher scope (e.g., at the top of the file) and reuse them in both functions.

const uniqueEventId = Math.floor(presentationTime / 1000); // Use timestamp-based unique ID

// Create the replace event WITHOUT maxDuration initially
const replaceEvent = {
schemeIdUri: 'urn:mpeg:dash:event:alternativeMPD:replace:2025',

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

For better maintainability and to avoid magic strings, please use the predefined constants from Constants.js.

  • On this line and line 69, urn:mpeg:dash:event:alternativeMPD:replace:2025 should be Constants.ALTERNATIVE_MPD.URIS.REPLACE.
  • On line 75, 'update' should be Constants.ALTERNATIVE_MPD.STATUS.UPDATE.
  • On lines 154 and 167, 'replace' should be Constants.ALTERNATIVE_MPD.MODES.REPLACE.
Suggested change
schemeIdUri: 'urn:mpeg:dash:event:alternativeMPD:replace:2025',
schemeIdUri: Constants.ALTERNATIVE_MPD.URIS.REPLACE,

timescale: 1000,
Event: [{
id: uniqueEventId,
presentationTime: presentationTime,
duration: duration,
ReplacePresentation: {
url: alternativeManifestUrl,
earliestResolutionTimeOffset: earliestResolutionTimeOffset,
// NOTE: No maxDuration set initially
clip: false,
}
}]
};

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

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

/**
* Utility function to inject a status update event with maxDuration during active playback
* This simulates the status="update" scenario where maxDuration is added mid-execution
* Instead of modifying the manifest, we inject the event via manifest update simulation
*/
function injectStatusUpdateEvent(player, originalManifestUrl, alternativeManifestUrl, presentationTime, newMaxDuration, callback) {
const mediaPlayer = player.player;

// Status updates should be processed like MPD updates
// So we simulate an MPD update that contains the status="update" event
mediaPlayer.retrieveManifest(originalManifestUrl, (manifest) => {
// Keep existing EventStreams and add the status update
if (!manifest.Period[0].EventStream) {
manifest.Period[0].EventStream = [];
}

const duration = 15000; // Keep same duration
const earliestResolutionTimeOffset = 3000;
const uniqueEventId = Math.floor(presentationTime / 1000); // Same ID as original event

// Create the status update event that will update the existing event
// This event should have the same ID as the original event but with status="update"
const statusUpdateEvent = {
schemeIdUri: 'urn:mpeg:dash:event:alternativeMPD:replace:2025',
timescale: 1000,
Event: [{
id: uniqueEventId, // Same ID as the original event to update it
presentationTime: presentationTime,
duration: duration,
status: 'update', // This marks it as an update event
ReplacePresentation: {
url: alternativeManifestUrl,
earliestResolutionTimeOffset: earliestResolutionTimeOffset,
maxDuration: newMaxDuration, // NEW: Add maxDuration via status update
clip: false,
}
}]
};

let existingEventStream = manifest.Period[0].EventStream.find(
stream => stream.schemeIdUri === 'urn:mpeg:dash:event:alternativeMPD:replace:2025'
);

if (existingEventStream) {
// Add the status update event to the existing EventStream
existingEventStream.Event.push(statusUpdateEvent.Event[0]);
} else {
// Add as a new EventStream (this creates the update scenario)
manifest.Period[0].EventStream.push(statusUpdateEvent);
}
Comment on lines +85 to +95

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This logic for adding the status update event is a bit confusing. The comment on line 57 (// Keep existing EventStreams...) is misleading because a fresh manifest is retrieved, so existingEventStream will always be undefined in this test's flow. The else block is always executed.

To improve clarity, you can simplify this block to directly add the new event stream, with a comment explaining why this works for an update.

Suggested change
let existingEventStream = manifest.Period[0].EventStream.find(
stream => stream.schemeIdUri === 'urn:mpeg:dash:event:alternativeMPD:replace:2025'
);
if (existingEventStream) {
// Add the status update event to the existing EventStream
existingEventStream.Event.push(statusUpdateEvent.Event[0]);
} else {
// Add as a new EventStream (this creates the update scenario)
manifest.Period[0].EventStream.push(statusUpdateEvent);
}
// Add the status update event as a new EventStream. The player will process this
// as an update because the event has status='update' and a matching ID to an active event.
manifest.Period[0].EventStream.push(statusUpdateEvent);


// Re-attach the modified manifest to trigger processing of the status update
mediaPlayer.attachSource(manifest);

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

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

describe(`Alternative MPD Status Update Live functionality tests for: ${name}`, () => {

let player;
let presentationTime;
let newMaxDuration;

before((done) => {
player = initializeDashJsAdapterForAlternativMedia(item, null);

// For live streams, use current time + offset to ensure the event is in the future
const currentPresentationTime = Date.now();
presentationTime = currentPresentationTime + 6000; // 6 seconds from now (longer to avoid timing issues)
newMaxDuration = 8000; // 8 seconds - shorter than original duration

injectInitialAlternativeMpdEvent(player, originalUrl, alternativeUrl, presentationTime, () => {
done();
});
});

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

it('should start alternative content without maxDuration, then update with maxDuration via status update and terminate early', (done) => {
let alternativeContentDetected = false;
let statusUpdateApplied = false;
let backToOriginalDetected = false;
let eventTriggered = false;
let alternativeStartTime = 0;
let alternativeEndTime = 0;
let updatedMaxDuration = null;

const timeout = setTimeout(() => {
done(new Error('Test timed out - status update live 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();

setTimeout(() => {
injectStatusUpdateEvent(player, originalUrl, alternativeUrl, presentationTime, newMaxDuration, () => {
statusUpdateApplied = true;
});
}, 3000);
}
});

player.registerEvent(Constants.ALTERNATIVE_MPD.CONTENT_END, (data) => {
if (data.event.mode === 'replace') {
alternativeEndTime = Date.now();
backToOriginalDetected = true;
updatedMaxDuration = data.event.maxDuration;

clearTimeout(timeout);

const actualAlternativeDuration = (alternativeEndTime - alternativeStartTime) / 1000;

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

// Verify that the status update was applied and maxDuration was set
const expectedMaxDurationInSeconds = newMaxDuration / 1000;
expect(updatedMaxDuration).to.equal(expectedMaxDurationInSeconds);

// Verify that alternative content terminated early due to maxDuration from status update
expect(actualAlternativeDuration).to.be.lessThan(10); // Much less than original 15s
expect(actualAlternativeDuration).to.be.lessThan(12);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This assertion is redundant because the preceding assertion on line 188 (expect(actualAlternativeDuration).to.be.lessThan(10)) is stricter. You can safely remove this line.


done();
}, 2000);
}
});

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

}, 45000); // Extended timeout for live content with status updates

});
});