Skip to content

Commit c326906

Browse files
authored
BADGER-254: Report quality changes more consistently (#415)
* feat: add downloadqualitychange plugin * fix: add mediakind * feat: set bitrate constraint before initialisation * style: fix typo * feat: add playback quality change plugin event * fix: correct parameter name * docs: add new plugin events to docs
1 parent 73d79c6 commit c326906

5 files changed

Lines changed: 129 additions & 23 deletions

File tree

docs/tutorials/02-plugins.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ The full interface is as follows:
1212
- `onPlayerInfoUpdated`
1313
- `onManifestLoaded`
1414
- `onManifestParseError`
15-
- `onQualityChangeRequested`
16-
- `onQualityChangedRendered`
15+
- `onQualityChangeRequested` (deprecated: use `onDownloadQualityChange`)
16+
- `onQualityChangeRendered` (deprecated: use `onPlaybackQualityChange`)
17+
- `onDownloadQualityChange`
18+
- `onPlaybackQualityChange`
1719
- `onSubtitlesLoadError`
1820
- `onSubtitlesTimeout`
1921
- `onSubtitlesXMLError`

src/playbackstrategy/msestrategy.js

Lines changed: 61 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import DOMHelpers from "../domhelpers"
1010
import Utils from "../utils/playbackutils"
1111
import convertTimeRangesToArray from "../utils/mse/convert-timeranges-to-array"
1212
import { ManifestType } from "../models/manifesttypes"
13+
import setPropertyPath from "../utils/setpropertypath"
1314

1415
const DEFAULT_SETTINGS = {
1516
liveDelay: 0,
@@ -85,6 +86,10 @@ function MSEStrategy(
8586
[MediaKinds.AUDIO]: undefined,
8687
[MediaKinds.VIDEO]: undefined,
8788
},
89+
playbackQuality: {
90+
[MediaKinds.AUDIO]: undefined,
91+
[MediaKinds.VIDEO]: undefined,
92+
},
8893
playbackBitrate: undefined,
8994
bufferLength: undefined,
9095
latency: undefined,
@@ -330,6 +335,8 @@ function MSEStrategy(
330335
mediaPlayer.setMediaDuration(Number.MAX_SAFE_INTEGER)
331336
}
332337

338+
DebugTool.info("Stream initialised")
339+
333340
if (mediaPlayer.getActiveStream()?.getHasVideoTrack()) {
334341
dispatchDownloadQualityChangeForKind(MediaKinds.VIDEO)
335342
dispatchMaxQualityChangeForKind(MediaKinds.VIDEO)
@@ -378,6 +385,43 @@ function MSEStrategy(
378385
const switchToPart = ` to ${qualityIndex} (${(bitrateInBps / 1000).toFixed(0)} kbps)`
379386

380387
DebugTool.info(`${abrChangePart}${switchFromPart}${switchToPart}`)
388+
389+
Plugins.interface.onDownloadQualityChange({
390+
type: "downloadqualitychange",
391+
detail: {
392+
mediaType: kind,
393+
currentBitrateInBps: bitrateInBps,
394+
currentQualityIndex: qualityIndex,
395+
previousBitrateInBps: prevBitrateInBps,
396+
previousQualityIndex: prevQualityIndex,
397+
},
398+
})
399+
}
400+
401+
function dispatchPlaybackQualityChangeForKind(kind, { qualityIndex } = {}) {
402+
const { qualityIndex: previousQualityIndex, bitrateInBps: previousBitrateInBps } =
403+
playerMetadata.playbackQuality[kind] ?? {}
404+
405+
if (previousQualityIndex === qualityIndex) {
406+
return
407+
}
408+
409+
const bitrateInBps = playbackBitrateForRepresentationIndex(qualityIndex, kind)
410+
411+
playerMetadata.playbackQuality[kind] = { bitrateInBps, qualityIndex }
412+
413+
DebugTool.dynamicMetric(`${kind}-playback-quality`, [qualityIndex, bitrateInBps])
414+
415+
Plugins.interface.onPlaybackQualityChange({
416+
type: "playbackqualitychange",
417+
detail: {
418+
mediaType: kind,
419+
previousBitrateInBps,
420+
previousQualityIndex,
421+
currentBitrateInBps: bitrateInBps,
422+
currentQualityIndex: qualityIndex,
423+
},
424+
})
381425
}
382426

383427
function dispatchMaxQualityChangeForKind(kind) {
@@ -428,23 +472,16 @@ function MSEStrategy(
428472
}
429473

430474
function onQualityChangeRendered(event) {
431-
if (
432-
event.newQuality !== undefined &&
433-
(event.mediaType === MediaKinds.AUDIO || event.mediaType === MediaKinds.VIDEO)
434-
) {
435-
const { mediaType, newQuality } = event
436-
437-
DebugTool.dynamicMetric(`${mediaType}-playback-quality`, [
438-
newQuality,
439-
playbackBitrateForRepresentationIndex(newQuality, mediaType),
440-
])
475+
const { mediaType, newQuality } = event
441476

477+
if (newQuality !== undefined && (mediaType === MediaKinds.AUDIO || mediaType === MediaKinds.VIDEO)) {
478+
dispatchPlaybackQualityChangeForKind(mediaType, { qualityIndex: newQuality })
442479
dispatchMaxQualityChangeForKind(mediaType)
443480
}
444481

445482
emitPlayerInfo()
446483

447-
Plugins.interface.onQualityChangedRendered(event)
484+
Plugins.interface.onQualityChangeRendered(event)
448485
}
449486

450487
/**
@@ -1021,17 +1058,22 @@ function MSEStrategy(
10211058
* Set constrained audio or video bitrate
10221059
*/
10231060
function setBitrateConstraint(mediaKind, minBitrateKbps, maxBitrateKbps) {
1061+
if (mediaKind !== MediaKinds.AUDIO && mediaKind !== MediaKinds.VIDEO) {
1062+
throw new TypeError(`Bitrate constraint not supported for this media kind. (got ${mediaKind})`)
1063+
}
1064+
1065+
if (mediaPlayer == null) {
1066+
setPropertyPath(playerSettings, ["streaming", "abr", "minBitrate", mediaKind], minBitrateKbps)
1067+
setPropertyPath(playerSettings, ["streaming", "abr", "maxBitrate", mediaKind], maxBitrateKbps)
1068+
1069+
return
1070+
}
1071+
10241072
mediaPlayer.updateSettings({
10251073
streaming: {
10261074
abr: {
1027-
minBitrate: {
1028-
audio: mediaKind === MediaKinds.AUDIO ? minBitratKbps : -1,
1029-
video: mediaKind === MediaKinds.VIDEO ? minBitrateKbps : -1,
1030-
},
1031-
maxBitrate: {
1032-
audio: mediaKind === MediaKinds.AUDIO ? maxBitrateKbps : -1,
1033-
video: mediaKind === MediaKinds.VIDEO ? maxBitrateKbps : -1,
1034-
},
1075+
minBitrate: { [mediaKind]: minBitrateKbps },
1076+
maxBitrate: { [mediaKind]: maxBitrateKbps },
10351077
},
10361078
},
10371079
})

src/playbackstrategy/msestrategy.test.js

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1923,12 +1923,34 @@ describe("Media Source Extensions Playback Strategy", () => {
19231923
})
19241924

19251925
describe("Playback bitrate", () => {
1926+
it("setBitrateConstraint should apply to an uninitialised player", () => {
1927+
const mseStrategy = MSEStrategy(mockMediaSources, MediaKinds.VIDEO, playbackElement)
1928+
1929+
mseStrategy.setBitrateConstraint("video", 100, 200)
1930+
1931+
mseStrategy.load()
1932+
1933+
expect(mockDashInstance.updateSettings).toHaveBeenCalledWith(
1934+
expect.objectContaining({
1935+
streaming: expect.objectContaining({
1936+
abr: expect.objectContaining({ minBitrate: { video: 100 }, maxBitrate: { video: 200 } }),
1937+
}),
1938+
})
1939+
)
1940+
})
1941+
19261942
it("setBitrateConstraint should call media player updateSettings", () => {
19271943
const mseStrategy = MSEStrategy(mockMediaSources, MediaKinds.VIDEO, playbackElement)
19281944
mseStrategy.load(null, 0)
19291945
mseStrategy.setBitrateConstraint("video", 100, 200)
19301946

1931-
expect(mockDashInstance.updateSettings).toHaveBeenCalled()
1947+
expect(mockDashInstance.updateSettings).toHaveBeenCalledWith(
1948+
expect.objectContaining({
1949+
streaming: expect.objectContaining({
1950+
abr: expect.objectContaining({ minBitrate: { video: 100 }, maxBitrate: { video: 200 } }),
1951+
}),
1952+
})
1953+
)
19321954
})
19331955

19341956
it("getPlaybackBitrate returns the current playback bitrate in kbps", () => {

src/plugins.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,9 @@ export default {
4040
onPlayerInfoUpdated: (evt) => callOnAllPlugins("onPlayerInfoUpdated", evt),
4141
onManifestLoaded: (manifest) => callOnAllPlugins("onManifestLoaded", manifest),
4242
onManifestParseError: (evt) => callOnAllPlugins("onManifestParseError", evt),
43+
onDownloadQualityChange: (evt) => callOnAllPlugins("onDownloadQualityChange", evt),
4344
onQualityChangeRequested: (evt) => callOnAllPlugins("onQualityChangeRequested", evt),
44-
onQualityChangedRendered: (evt) => callOnAllPlugins("onQualityChangedRendered", evt),
45+
onQualityChangeRendered: (evt) => callOnAllPlugins("onQualityChangeRendered", evt),
4546
onSubtitlesLoadError: (evt) => callOnAllPlugins("onSubtitlesLoadError", evt),
4647
onSubtitlesTimeout: (evt) => callOnAllPlugins("onSubtitlesTimeout", evt),
4748
onSubtitlesXMLError: (evt) => callOnAllPlugins("onSubtitlesXMLError", evt),
@@ -50,6 +51,7 @@ export default {
5051
onSubtitlesDynamicLoadError: (evt) => callOnAllPlugins("onSubtitlesDynamicLoadError", evt),
5152
onFragmentContentLengthMismatch: (evt) => callOnAllPlugins("onFragmentContentLengthMismatch", evt),
5253
onQuotaExceeded: (evt) => callOnAllPlugins("onQuotaExceeded", evt),
54+
onPlaybackQualityChange: (evt) => callOnAllPlugins("onPlaybackQualityChange", evt),
5355
onPlaybackRateChanged: (evt) => callOnAllPlugins("onPlaybackRateChanged", evt),
5456
onPlaybackFrozen: (evt) => callOnAllPlugins("onPlaybackFrozen", evt),
5557
},

src/utils/setpropertypath.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
export default function setPropertyPath<Key extends string>(
2+
target: Record<Key, unknown>,
3+
path: Key | Key[],
4+
value: unknown
5+
) {
6+
if (target == null || (typeof target !== "object" && typeof target !== "function")) {
7+
throw new TypeError(`Target must be an object. (got ${target as any})`)
8+
}
9+
10+
const keysDesc = typeof path === "string" ? path.split(".") : path
11+
12+
if (keysDesc.length === 0) {
13+
throw new TypeError("Empty path provided. (got [])")
14+
}
15+
16+
const key = keysDesc.shift()
17+
18+
if (key == null) {
19+
throw new TypeError(`Cannot index object with key. (got key '${key}')`)
20+
}
21+
22+
if (keysDesc.length === 0) {
23+
target[key as Key] = value
24+
25+
return
26+
}
27+
28+
let next = target[key as Key]
29+
30+
if (next != null && typeof next !== "object" && typeof next !== "function") {
31+
throw new TypeError(`Cannot assign to primitive value. (got '${next as string}')`)
32+
}
33+
34+
next ??= {}
35+
target[key as Key] = next
36+
37+
setPropertyPath(next as Record<Key, unknown>, keysDesc as Key | Key[], value)
38+
}

0 commit comments

Comments
 (0)