From 326325d6ede1a90c32933041e442ff715aee0a9b Mon Sep 17 00:00:00 2001 From: CY Date: Wed, 11 Sep 2024 16:20:28 +0800 Subject: [PATCH] feat(hls,dash): sort quality --- packages/dash/README.md | 49 +------ packages/dash/package.json | 2 +- packages/dash/src/index.ts | 138 +++++++++++++------ packages/docs/src/pages/docs/guide/start.mdx | 3 +- packages/docs/src/pages/docs/ui.mdx | 6 +- packages/hls/package.json | 2 +- packages/hls/src/index.ts | 34 ++--- 7 files changed, 125 insertions(+), 109 deletions(-) diff --git a/packages/dash/README.md b/packages/dash/README.md index dfadb831..35e522f1 100644 --- a/packages/dash/README.md +++ b/packages/dash/README.md @@ -15,8 +15,6 @@ npm i @oplayer/core @oplayer/dash dashjs ```html - -
@@ -28,52 +26,7 @@ npm i @oplayer/core @oplayer/dash dashjs poster: 'https://cdn.jsdelivr.net/gh/shiyiya/QI-ABSL@master/o/poster.png' } }) - .use([ODash()]) + .use([ODash({ library: 'https://cdn.dashjs.org/latest/dash.all.min.js' })]) .create() ``` - -## Usage - -```ts -export type Matcher = (video: HTMLVideoElement, source: Source) => boolean - -// active inactive -export type Active = ( - instance: MediaPlayerClass, - library: typeof import('dashjs') -) => void | ((instance: MediaPlayerClass, library: typeof import('dashjs')) => void) - -export interface DashPluginOptions { - matcher?: Matcher - /** - * config for dashjs - * - * @type {MediaPlayerSettingClass} - */ - config?: MediaPlayerSettingClass - /** - * enable quality control for the stream, does not apply to the native (iPhone) clients. - * @default: true - */ - qualityControl?: boolean - /** - * control how the stream quality is switched. default: immediate - * @value immediate: Trigger an immediate quality level switch to new quality level. This will abort the current fragment request if any, flush the whole buffer, and fetch fragment matching with current position and requested quality level. - * @value smooth: Trigger a quality level switch for next fragment. This could eventually flush already buffered next fragment. - */ - qualitySwitch?: 'immediate' | 'smooth' - /** - * @default: false - */ - withBitrate?: boolean - /** - * @default: true - */ - audioControl?: boolean - /** - * @default: true - */ - textControl?: boolean -} -``` diff --git a/packages/dash/package.json b/packages/dash/package.json index db7fa702..31a530e8 100644 --- a/packages/dash/package.json +++ b/packages/dash/package.json @@ -1,6 +1,6 @@ { "name": "@oplayer/dash", - "version": "1.2.25", + "version": "1.2.26-beta.0", "description": "Dash plugin for oplayer", "type": "module", "main": "./dist/index.es.js", diff --git a/packages/dash/src/index.ts b/packages/dash/src/index.ts index f73f3439..183d4fe3 100644 --- a/packages/dash/src/index.ts +++ b/packages/dash/src/index.ts @@ -1,5 +1,10 @@ import { loadSDK, type Player, type PlayerPlugin, type RequiredPartial, type Source } from '@oplayer/core' -import type { MediaPlayerClass, MediaPlayerSettingClass, QualityChangeRenderedEvent } from 'dashjs' +import type { + BitrateInfo, + MediaPlayerClass, + MediaPlayerSettingClass, + QualityChangeRenderedEvent +} from 'dashjs' const PLUGIN_NAME = 'oplayer-plugin-dash' @@ -8,6 +13,22 @@ export type Matcher = (video: HTMLVideoElement, source: Source) => boolean export interface DashPluginOptions { library?: string matcher?: Matcher + + /** + * default auto + */ + defaultQuality?: (levels: BitrateInfo[]) => number + + /** + * default browser language + */ + defaultAudio?: (tracks: dashjs.MediaInfo[]) => number + + /** + * default browser language + */ + defaultSubtitle?: (tracks: dashjs.MediaInfo[]) => number + /** * config for dashjs * @@ -68,7 +89,10 @@ class DashPlugin implements PlayerPlugin { qualityControl: true, withBitrate: false, qualitySwitch: 'immediate', - matcher: defaultMatcher + matcher: defaultMatcher, + defaultQuality: () => -1, + defaultAudio: () => -1, + defaultSubtitle: () => -1 } constructor(options?: DashPluginOptions) { @@ -127,46 +151,49 @@ function getSettingsByType(instance: MediaPlayerClass, type: 'video', withBitrat const isAuto = Boolean(instance.getSettings().streaming?.abr?.autoSwitchBitrate?.video) const videoQuality = instance.getQualityFor('video') if (bitrateInfoList.length > 1) { - return bitrateInfoList.map((it) => { - let name = it.height + 'p' - - if (withBitrate) { - const kb = it.bitrate / 1000 - const useMb = kb > 1000 - const number = useMb ? (kb / 1000).toFixed(2) : Math.floor(kb) - name += ` (${number}${useMb ? 'm' : 'k'}bps)` - } + return bitrateInfoList + .toSorted((a, b) => b.bitrate - a.bitrate) + .map((it) => { + let name = it.height + 'p' + + if (withBitrate) { + const kb = it.bitrate / 1000 + const useMb = kb > 1000 + const number = useMb ? (kb / 1000).toFixed(2) : Math.floor(kb) + name += ` (${number}${useMb ? 'm' : 'k'}bps)` + } - return { - name, - default: isAuto ? false : videoQuality == it.qualityIndex, - value: it.qualityIndex - } - }) + return { + name, + default: isAuto ? false : videoQuality == it.qualityIndex, + value: it.qualityIndex + } + }) } return [] } -const generateSetting = (player: Player, instance: MediaPlayerClass, options: DashPluginOptions) => { +const generateSetting = (player: Player, instance: MediaPlayerClass, options: DashPlugin['options']) => { instance.on(DashPlugin.library.MediaPlayer.events.STREAM_INITIALIZED, function () { if (options.qualityControl) { + const quality = instance.getBitrateInfoListFor('video') + if (quality.length < 2) return + + const defaultLevel = options.defaultQuality(quality) + if (defaultLevel != -1) instance.setQualityFor('video', defaultLevel) + settingUpdater({ name: 'Quality', icon: player.context.ui.icons.quality, - settings() { - const ex = getSettingsByType(instance, 'video', options.withBitrate) - - if (ex.length) { - ex.unshift({ + settings: () => + [ + { name: player.locales.get('Auto'), default: Boolean(instance.getSettings().streaming?.abr?.autoSwitchBitrate?.video), value: -1 - }) - } - - return ex - }, + } + ].concat(getSettingsByType(instance, 'video', options.withBitrate)), onChange({ value }) { instance.updateSettings({ streaming: { abr: { autoSwitchBitrate: { video: value == -1 } } } @@ -191,11 +218,27 @@ const generateSetting = (player: Player, instance: MediaPlayerClass, options: Da } if (options.audioControl) { + const audioTracks = instance.getTracksFor('audio') + if (audioTracks.length < 2) return + + let defaultAudio: number | undefined = options.defaultAudio(audioTracks) + if (defaultAudio == -1) { + defaultAudio = audioTracks.find(({ lang }) => { + return lang === navigator.language || lang === navigator.language.split('-')[0] + })?.id as unknown as number + } + + if (defaultAudio != -1 && defaultAudio != undefined) { + instance.setCurrentTrack(audioTracks.find((t) => (t.id as unknown as number) == defaultAudio)!) + } + + console.log(audioTracks!) + settingUpdater({ name: 'Language', icon: player.context.ui.icons.lang, settings() { - return instance.getTracksFor('audio').map((it) => ({ + return audioTracks.map((it) => ({ name: it.lang || 'unknown', default: instance.getCurrentTrackFor('audio')?.id == it.id, value: it @@ -208,23 +251,38 @@ const generateSetting = (player: Player, instance: MediaPlayerClass, options: Da } if (options.textControl) { + const textTracks = instance.getTracksFor('text') + if (textTracks.length < 1) return + + let defaultSubtitle: number | undefined = options.defaultSubtitle(textTracks) + if (defaultSubtitle == -1) { + defaultSubtitle = textTracks.find(({ lang }) => { + return lang === navigator.language || lang === navigator.language.split('-')[0] + })?.id as unknown as number + } + + if (defaultSubtitle != -1 && defaultSubtitle != undefined) { + instance.enableText(true) + instance.setTextTrack(defaultSubtitle) + } + settingUpdater({ name: 'Subtitle', icon: player.context.ui.icons.subtitle, settings() { - const ex = instance.getTracksFor('text').map((it) => ({ - name: it.lang || 'unknown', - default: instance.getCurrentTrackFor('text')?.id == it.id, - value: it.id - })) - if (ex.length) { - ex.unshift({ + return [ + { name: player.locales.get('Off'), default: !instance.isTextEnabled(), value: -1 as any - }) - } - return ex + } + ].concat( + textTracks.map((it) => ({ + name: it.lang || 'unknown', + default: instance.getCurrentTrackFor('text')?.id == it.id, + value: it.id + })) + ) }, onChange({ value }) { instance.enableText(value != -1) @@ -245,8 +303,6 @@ const generateSetting = (player: Player, instance: MediaPlayerClass, options: Da onChange: (it: { value: any }) => void }) { const settings = arg.settings() - if (settings.length < 2) return - const { name, icon, onChange } = arg player.context.ui.setting.unregister(`${PLUGIN_NAME}-${name}`) diff --git a/packages/docs/src/pages/docs/guide/start.mdx b/packages/docs/src/pages/docs/guide/start.mdx index a5ef2f82..c2c20b82 100644 --- a/packages/docs/src/pages/docs/guide/start.mdx +++ b/packages/docs/src/pages/docs/guide/start.mdx @@ -52,7 +52,8 @@ Player.make('#oplayer', { ```html filename="app.html" - + + - + + + + ``` ## Basic diff --git a/packages/hls/package.json b/packages/hls/package.json index 279c6d0e..e97f163f 100644 --- a/packages/hls/package.json +++ b/packages/hls/package.json @@ -1,6 +1,6 @@ { "name": "@oplayer/hls", - "version": "1.2.27-beta.2", + "version": "1.2.27-beta.3", "description": "Hls plugin for oplayer", "type": "module", "main": "./dist/index.es.js", diff --git a/packages/hls/src/index.ts b/packages/hls/src/index.ts index 3b70e8c6..dbbdc3b9 100644 --- a/packages/hls/src/index.ts +++ b/packages/hls/src/index.ts @@ -211,22 +211,24 @@ const generateSetting = (player: Player, instance: Hls, options: HlsPlugin['opti icon: ui.icons.quality, name: 'Quality', settings() { - return instance.levels.reduce( - (pre, level, id) => { - let name = (level.name || level.height).toString() - if (isFinite(+name)) name += 'p' - if (options.withBitrate) { - const kb = level.bitrate / 1000 - const useMb = kb > 1000 - const number = useMb ? (kb / 1000).toFixed(2) : Math.floor(kb) - name += ` (${number}${useMb ? 'm' : 'k'}bps)` - } - - pre.push({ name, default: defaultLevel == id, value: id }) - return pre - }, - [{ name: player.locales.get('Auto'), default: instance.autoLevelEnabled, value: -1 }] - ) + return instance.levels + .toSorted((a, b) => b.bitrate - a.bitrate) + .reduce( + (pre, level, id) => { + let name = (level.name || level.height).toString() + if (isFinite(+name)) name += 'p' + if (options.withBitrate) { + const kb = level.bitrate / 1000 + const useMb = kb > 1000 + const number = useMb ? (kb / 1000).toFixed(2) : Math.floor(kb) + name += ` (${number}${useMb ? 'm' : 'k'}bps)` + } + + pre.push({ name, default: defaultLevel == id, value: id }) + return pre + }, + [{ name: player.locales.get('Auto'), default: instance.autoLevelEnabled, value: -1 }] + ) }, onChange(it) { if (options.qualitySwitch == 'immediate') {