Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
2 changes: 1 addition & 1 deletion docs/tutorials/Plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ The full interface is as follows:
- `onManifestLoaded`
- `onManifestParseError`
- `onQualityChangeRequested`
- `onQualityChangedRendered`
- `onQualityChangeRendered`
- `onSubtitlesLoadError`
- `onSubtitlesTimeout`
- `onSubtitlesXMLError`
Expand Down
80 changes: 61 additions & 19 deletions src/playbackstrategy/msestrategy.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import DOMHelpers from "../domhelpers"
import Utils from "../utils/playbackutils"
import convertTimeRangesToArray from "../utils/mse/convert-timeranges-to-array"
import { ManifestType } from "../models/manifesttypes"
import setPropertyPath from "../utils/setpropertypath"

const DEFAULT_SETTINGS = {
liveDelay: 0,
Expand Down Expand Up @@ -85,6 +86,10 @@ function MSEStrategy(
[MediaKinds.AUDIO]: undefined,
[MediaKinds.VIDEO]: undefined,
},
playbackQuality: {
[MediaKinds.AUDIO]: undefined,
[MediaKinds.VIDEO]: undefined,
},
playbackBitrate: undefined,
bufferLength: undefined,
latency: undefined,
Expand Down Expand Up @@ -330,6 +335,8 @@ function MSEStrategy(
mediaPlayer.setMediaDuration(Number.MAX_SAFE_INTEGER)
}

DebugTool.info("Stream initialised")

if (mediaPlayer.getActiveStream()?.getHasVideoTrack()) {
dispatchDownloadQualityChangeForKind(MediaKinds.VIDEO)
dispatchMaxQualityChangeForKind(MediaKinds.VIDEO)
Expand Down Expand Up @@ -378,6 +385,43 @@ function MSEStrategy(
const switchToPart = ` to ${qualityIndex} (${(bitrateInBps / 1000).toFixed(0)} kbps)`

DebugTool.info(`${abrChangePart}${switchFromPart}${switchToPart}`)

Plugins.interface.onDownloadQualityChange({
type: "downloadqualitychange",
detail: {
mediaType: kind,
currentBitrateInBps: bitrateInBps,
currentQualityIndex: qualityIndex,
previousBitrateInBps: prevBitrateInBps,
previousQualityIndex: prevQualityIndex,
},
})
}

function dispatchPlaybackQualityChangeForKind(kind, { qualityIndex } = {}) {
const { qualityIndex: previousQualityIndex, bitrateInBps: previousBitrateInBps } =
playerMetadata.playbackQuality[kind] ?? {}

if (previousQualityIndex === qualityIndex) {
return
}

const bitrateInBps = playbackBitrateForRepresentationIndex(qualityIndex, kind)

playerMetadata.playbackQuality[kind] = { bitrateInBps, qualityIndex }

DebugTool.dynamicMetric(`${kind}-playback-quality`, [qualityIndex, bitrateInBps])

Plugins.interface.onPlaybackQualityChange({
type: "playbackqualitychange",
detail: {
mediaType: kind,
previousBitrateInBps,
previousQualityIndex,
currentBitrateInBps: bitrateInBps,
currentQualityIndex: qualityIndex,
},
})
}

function dispatchMaxQualityChangeForKind(kind) {
Expand Down Expand Up @@ -428,23 +472,16 @@ function MSEStrategy(
}

function onQualityChangeRendered(event) {
if (
event.newQuality !== undefined &&
(event.mediaType === MediaKinds.AUDIO || event.mediaType === MediaKinds.VIDEO)
) {
const { mediaType, newQuality } = event

DebugTool.dynamicMetric(`${mediaType}-playback-quality`, [
newQuality,
playbackBitrateForRepresentationIndex(newQuality, mediaType),
])
const { mediaType, newQuality } = event

if (newQuality !== undefined && (mediaType === MediaKinds.AUDIO || mediaType === MediaKinds.VIDEO)) {
dispatchPlaybackQualityChangeForKind(mediaType, { qualityIndex: newQuality })
dispatchMaxQualityChangeForKind(mediaType)
}

emitPlayerInfo()

Plugins.interface.onQualityChangedRendered(event)
Plugins.interface.onQualityChangeRendered(event)
}

/**
Expand Down Expand Up @@ -1021,17 +1058,22 @@ function MSEStrategy(
* Set constrained audio or video bitrate
*/
function setBitrateConstraint(mediaKind, minBitrateKbps, maxBitrateKbps) {
if (mediaKind !== MediaKinds.AUDIO && mediaKind !== MediaKinds.VIDEO) {
throw new TypeError(`Bitrate constraint not supported for this media kind. (got ${mediaKind})`)
}

if (mediaPlayer == null) {
setPropertyPath(playerSettings, ["streaming", "abr", "minBitrate", mediaKind], minBitrateKbps)
setPropertyPath(playerSettings, ["streaming", "abr", "maxBitrate", mediaKind], maxBitrateKbps)

return
}

mediaPlayer.updateSettings({
streaming: {
abr: {
minBitrate: {
audio: mediaKind === MediaKinds.AUDIO ? minBitratKbps : -1,
video: mediaKind === MediaKinds.VIDEO ? minBitrateKbps : -1,
},
maxBitrate: {
audio: mediaKind === MediaKinds.AUDIO ? maxBitrateKbps : -1,
video: mediaKind === MediaKinds.VIDEO ? maxBitrateKbps : -1,
},
minBitrate: { [mediaKind]: minBitrateKbps },
maxBitrate: { [mediaKind]: maxBitrateKbps },
},
},
})
Expand Down
24 changes: 23 additions & 1 deletion src/playbackstrategy/msestrategy.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1923,12 +1923,34 @@ describe("Media Source Extensions Playback Strategy", () => {
})

describe("Playback bitrate", () => {
it("setBitrateConstraint should apply to an uninitialised player", () => {
const mseStrategy = MSEStrategy(mockMediaSources, MediaKinds.VIDEO, playbackElement)

mseStrategy.setBitrateConstraint("video", 100, 200)

mseStrategy.load()

expect(mockDashInstance.updateSettings).toHaveBeenCalledWith(
expect.objectContaining({
streaming: expect.objectContaining({
abr: expect.objectContaining({ minBitrate: { video: 100 }, maxBitrate: { video: 200 } }),
}),
})
)
})

it("setBitrateConstraint should call media player updateSettings", () => {
const mseStrategy = MSEStrategy(mockMediaSources, MediaKinds.VIDEO, playbackElement)
mseStrategy.load(null, 0)
mseStrategy.setBitrateConstraint("video", 100, 200)

expect(mockDashInstance.updateSettings).toHaveBeenCalled()
expect(mockDashInstance.updateSettings).toHaveBeenCalledWith(
expect.objectContaining({
streaming: expect.objectContaining({
abr: expect.objectContaining({ minBitrate: { video: 100 }, maxBitrate: { video: 200 } }),
}),
})
)
})

it("getPlaybackBitrate returns the current playback bitrate in kbps", () => {
Expand Down
4 changes: 3 additions & 1 deletion src/plugins.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ export default {
onPlayerInfoUpdated: (evt) => callOnAllPlugins("onPlayerInfoUpdated", evt),
onManifestLoaded: (manifest) => callOnAllPlugins("onManifestLoaded", manifest),
onManifestParseError: (evt) => callOnAllPlugins("onManifestParseError", evt),
onDownloadQualityChange: (evt) => callOnAllPlugins("onDownloadQualityChange", evt),
onQualityChangeRequested: (evt) => callOnAllPlugins("onQualityChangeRequested", evt),
onQualityChangedRendered: (evt) => callOnAllPlugins("onQualityChangedRendered", evt),
onQualityChangeRendered: (evt) => callOnAllPlugins("onQualityChangeRendered", evt),
onSubtitlesLoadError: (evt) => callOnAllPlugins("onSubtitlesLoadError", evt),
onSubtitlesTimeout: (evt) => callOnAllPlugins("onSubtitlesTimeout", evt),
onSubtitlesXMLError: (evt) => callOnAllPlugins("onSubtitlesXMLError", evt),
Expand All @@ -50,6 +51,7 @@ export default {
onSubtitlesDynamicLoadError: (evt) => callOnAllPlugins("onSubtitlesDynamicLoadError", evt),
onFragmentContentLengthMismatch: (evt) => callOnAllPlugins("onFragmentContentLengthMismatch", evt),
onQuotaExceeded: (evt) => callOnAllPlugins("onQuotaExceeded", evt),
onPlaybackQualityChange: (evt) => callOnAllPlugins("onPlaybackQualityChange", evt),
onPlaybackRateChanged: (evt) => callOnAllPlugins("onPlaybackRateChanged", evt),
onPlaybackFrozen: (evt) => callOnAllPlugins("onPlaybackFrozen", evt),
},
Expand Down
38 changes: 38 additions & 0 deletions src/utils/setpropertypath.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
export default function setPropertyPath<Key extends string>(
target: Record<Key, unknown>,
path: Key | Key[],
value: unknown
) {
if (target == null || (typeof target !== "object" && typeof target !== "function")) {
throw new TypeError(`Target must be an object. (got ${target as any})`)
}

const keysDesc = typeof path === "string" ? path.split(".") : path

if (keysDesc.length === 0) {
throw new TypeError("Empty path provided. (got [])")
}

const key = keysDesc.shift()

if (key == null) {
throw new TypeError(`Cannot index object with key. (got key '${key}')`)
}

if (keysDesc.length === 0) {
target[key as Key] = value

return
}

let next = target[key as Key]

if (next != null && typeof next !== "object" && typeof next !== "function") {
throw new TypeError(`Cannot assign to primitive value. (got '${next as string}')`)
}

next ??= {}
target[key as Key] = next

setPropertyPath(next as Record<Key, unknown>, keysDesc as Key | Key[], value)
}