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
6 changes: 4 additions & 2 deletions docs/tutorials/02-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ The full interface is as follows:
- `onPlayerInfoUpdated`
- `onManifestLoaded`
- `onManifestParseError`
- `onQualityChangeRequested`
- `onQualityChangedRendered`
- `onQualityChangeRequested` (deprecated: use `onDownloadQualityChange`)
- `onQualityChangeRendered` (deprecated: use `onPlaybackQualityChange`)
- `onDownloadQualityChange`
- `onPlaybackQualityChange`
- `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)
}
Loading