diff --git a/.vscode/settings.json b/.vscode/settings.json
index 8c46f0f08b..f6c54aa57a 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -25,11 +25,14 @@
],
"cSpell.words": [
"artalk",
+ "artplayer",
+ "bilibili",
"bumpp",
"commitlint",
"chartjs",
"composables",
"darkmode",
+ "dashjs",
"devtool",
"docsearch",
"domhandler",
@@ -60,6 +63,7 @@
"meilisearch",
"meteorlxy",
"mhchem",
+ "mpegts",
"mindmap",
"nord",
"npmmirror",
@@ -90,6 +94,7 @@
"twoslash",
"umami",
"unmount",
+ "vidstack",
"vuejs",
"vuepress",
"vueuse",
diff --git a/plugins/features/plugin-media/package.json b/plugins/features/plugin-media/package.json
new file mode 100644
index 0000000000..4ced26d953
--- /dev/null
+++ b/plugins/features/plugin-media/package.json
@@ -0,0 +1,78 @@
+{
+ "name": "@vuepress/plugin-media",
+ "version": "2.0.0-rc.91",
+ "description": "VuePress plugin - media",
+ "keywords": [
+ "vuepress-plugin",
+ "vuepress",
+ "plugin",
+ "medium",
+ "zoom",
+ "image"
+ ],
+ "homepage": "https://ecosystem.vuejs.press/plugins/features/media.html",
+ "bugs": {
+ "url": "https://github.com/vuepress/ecosystem/issues"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/vuepress/ecosystem.git",
+ "directory": "plugins/features/plugin-media"
+ },
+ "license": "MIT",
+ "author": {
+ "name": "Mr.Hope",
+ "email": "mister-hope@outlook.com",
+ "url": "https://mister-hope.com"
+ },
+ "type": "module",
+ "exports": {
+ ".": "./lib/node/index.js",
+ "./client": "./lib/client/index.js",
+ "./package.json": "./package.json"
+ },
+ "main": "./lib/node/index.js",
+ "types": "./lib/node/index.d.ts",
+ "files": [
+ "lib"
+ ],
+ "scripts": {
+ "build": "tsc -b tsconfig.build.json",
+ "bundle": "rollup -c rollup.config.ts --configPlugin esbuild",
+ "clean": "rimraf --glob ./lib ./*.tsbuildinfo",
+ "copy": "cpx \"src/**/*.css\" lib"
+ },
+ "dependencies": {
+ "@vuepress/helper": "workspace:*",
+ "@vueuse/core": "catalog:",
+ "vue": "catalog:"
+ },
+ "peerDependencies": {
+ "artplayer": "^5.3.0",
+ "dashjs": "4.7.4",
+ "hls.js": "^1.6.13",
+ "mpegts.js": "^1.7.3",
+ "vidstack": "^1.12.13",
+ "vuepress": "catalog:"
+ },
+ "peerDependenciesMeta": {
+ "artplayer": {
+ "optional": true
+ },
+ "dashjs": {
+ "optional": true
+ },
+ "hls.js": {
+ "optional": true
+ },
+ "mpegts.js": {
+ "optional": true
+ },
+ "vidstack": {
+ "optional": true
+ }
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/plugins/features/plugin-media/rollup.config.ts b/plugins/features/plugin-media/rollup.config.ts
new file mode 100644
index 0000000000..86a1f5ac55
--- /dev/null
+++ b/plugins/features/plugin-media/rollup.config.ts
@@ -0,0 +1,9 @@
+import { rollupBundle } from '../../../scripts/rollup.js'
+
+export default [
+ ...rollupBundle('node/index'),
+ ...rollupBundle({
+ base: 'client',
+ files: ['config', 'index'],
+ }),
+]
diff --git a/plugins/features/plugin-media/src/client/components/ArtPlayer.ts b/plugins/features/plugin-media/src/client/components/ArtPlayer.ts
new file mode 100644
index 0000000000..70bb7fa081
--- /dev/null
+++ b/plugins/features/plugin-media/src/client/components/ArtPlayer.ts
@@ -0,0 +1,302 @@
+import { LoadingIcon, keys } from '@vuepress/helper/client'
+import type Artplayer from 'artplayer'
+import type { Option as ArtPlayerInitOptions } from 'artplayer'
+import type { PropType, VNode } from 'vue'
+import { camelize, defineComponent, h, onMounted, onUnmounted, ref } from 'vue'
+import { usePageLang } from 'vuepress/client'
+
+import type { ArtPlayerOptions } from '../../shared/index.js'
+import { useSize } from '../composables/index.js'
+import { getLink } from '../utils/getLink.js'
+import {
+ SUPPORTED_VIDEO_TYPES,
+ getTypeByUrl,
+ registerMseDash,
+ registerMseFlv,
+ registerMseHls,
+} from '../utils/registerMse.js'
+
+import '../styles/art-player.css'
+
+const BOOLEAN_TRUE_ATTRS = [
+ 'no-fullscreen',
+ 'no-hotkey',
+ 'no-playback-rate',
+ 'no-setting',
+ 'no-mutex',
+ 'no-plays-inline',
+] as const
+
+const BOOLEAN_FALSE_ATTRS = [
+ 'airplay',
+ 'autoplay',
+ 'aspect-ratio',
+ 'auto-mini',
+ 'auto-size',
+ 'auto-orientation',
+ 'auto-playback',
+ 'fast-forward',
+ 'flip',
+ 'fullscreen-web',
+ 'lock',
+ 'loop',
+ 'is-live',
+ 'muted',
+ 'mini-progress-bar',
+ 'pip',
+ 'screenshot',
+ 'subtitle-offset',
+] as const
+
+// Note: This should be updated with https://github.com/zhw2590582/ArtPlayer/blob/master/packages/artplayer/src/i18n/index.js
+const SUPPORTED_LANG_NAME = [
+ 'en',
+ 'pl',
+ 'cs',
+ 'es',
+ 'fa',
+ 'fr',
+ 'id',
+ 'ru',
+ 'tr',
+]
+const SUPPORTED_LANG_CODE = ['zh-cn', 'zh-tw']
+
+type KebabCaseToCamelCase<
+ S extends string,
+ Cap extends boolean = false,
+> = S extends `${infer Head}-${infer Tail}`
+ ? `${Cap extends true ? Capitalize
: Head}${KebabCaseToCamelCase<
+ Tail,
+ true
+ >}`
+ : Cap extends true
+ ? Capitalize
+ : S
+
+type ArtPlayerBooleanOptionKey =
+ | (typeof BOOLEAN_FALSE_ATTRS extends readonly (infer T extends string)[]
+ ? KebabCaseToCamelCase
+ : never)
+ | (typeof BOOLEAN_TRUE_ATTRS extends readonly (infer T extends string)[]
+ ? T extends `no-${infer Key}`
+ ? KebabCaseToCamelCase
+ : never
+ : never)
+
+declare const ART_PLAYER_OPTIONS: ArtPlayerOptions
+
+const getLang = (lang: string): string => {
+ const langCode = lang.toLowerCase()
+ const [langName] = langCode.split('-')
+
+ return SUPPORTED_LANG_CODE.includes(langCode)
+ ? langCode
+ : SUPPORTED_LANG_NAME.includes(langName)
+ ? langName
+ : langName === 'zh'
+ ? 'zh-cn'
+ : 'en'
+}
+
+export const ArtPlayer = defineComponent({
+ name: 'ArtPlayer',
+
+ inheritAttrs: false,
+
+ props: {
+ /**
+ * Video Source URL
+ *
+ * 视频源文件地址
+ */
+ src: {
+ type: String,
+ required: true,
+ },
+
+ /**
+ * Video Type
+ *
+ * 视频类型
+ */
+ type: String,
+
+ /**
+ * Video poster
+ *
+ * 视频封面
+ */
+ poster: String,
+
+ /**
+ * Video title
+ *
+ * 视频标题
+ */
+ title: String,
+
+ /**
+ * Component width
+ *
+ * 组件宽度
+ */
+ width: {
+ type: [String, Number],
+ default: '100%',
+ },
+
+ /**
+ * Component height
+ *
+ * 组件高度
+ */
+ height: [String, Number],
+
+ /**
+ * Component width / height ratio
+ *
+ * 组件长宽比
+ */
+ ratio: {
+ type: [String, Number],
+ default: 16 / 9,
+ },
+
+ /**
+ * ArtPlayer config
+ *
+ * ArtPlayer 配置
+ */
+ config: Object as PropType>,
+
+ /**
+ * Customize Artplayer
+ *
+ * 对 Artplayer 进行自定义
+ */
+ customPlayer: Function as PropType<
+ (
+ player: Artplayer,
+ ) => Artplayer | Promise | Promise | void
+ >,
+ },
+
+ setup(props, { attrs }) {
+ const lang = usePageLang()
+ const { el, width, height, resize } = useSize(props, 0)
+
+ const loaded = ref(false)
+ let artPlayerInstance: Artplayer | null = null
+
+ const getInitOptions = (): ArtPlayerInitOptions => {
+ const initOptions: ArtPlayerInitOptions = {
+ theme: '#3eaf7c',
+ ...ART_PLAYER_OPTIONS,
+
+ container: el.value!,
+ poster: props.poster,
+ url: getLink(props.src),
+ type: props.type ?? getTypeByUrl(props.src),
+ lang: getLang(lang.value),
+ ...props.config,
+ // This option must be set false to avoid problems
+ useSSR: false,
+ }
+
+ const attrsKeys = keys(attrs)
+
+ BOOLEAN_TRUE_ATTRS.forEach((config) => {
+ if (attrsKeys.includes(config))
+ initOptions[
+ camelize(config.replace(/^no-/, '')) as ArtPlayerBooleanOptionKey
+ ] = false
+ })
+ BOOLEAN_FALSE_ATTRS.forEach((config) => {
+ if (attrsKeys.includes(config))
+ initOptions[camelize(config) as ArtPlayerBooleanOptionKey] = true
+ })
+
+ // Auto config mse
+ if (initOptions.type) {
+ // eslint-disable-next-line no-multi-assign
+ const customType = (initOptions.customType ??= {})
+
+ if (SUPPORTED_VIDEO_TYPES.includes(initOptions.type.toLowerCase()))
+ switch (initOptions.type.toLowerCase()) {
+ case 'm3u8':
+ case 'hls':
+ customType[initOptions.type] ??= (
+ video: HTMLVideoElement,
+ src: string,
+ player: Artplayer,
+ ): Promise =>
+ registerMseHls(video, src, (destroy) => {
+ player.on('destroy', destroy)
+ })
+ break
+
+ case 'flv':
+ case 'ts':
+ customType[initOptions.type] ??= (
+ video: HTMLVideoElement,
+ src: string,
+ player: Artplayer,
+ ): Promise =>
+ registerMseFlv(video, src, (destroy) => {
+ player.on('destroy', destroy)
+ })
+ break
+
+ case 'mpd':
+ case 'dash':
+ customType[initOptions.type] ??= (
+ video: HTMLVideoElement,
+ src: string,
+ player: Artplayer,
+ ): Promise =>
+ registerMseDash(video, src, (destroy) => {
+ player.on('destroy', destroy)
+ })
+ break
+
+ default:
+ }
+ else
+ // eslint-disable-next-line no-console
+ console.warn(
+ `[components]: ArtPlayer does not support current file type ${initOptions.type}!`,
+ )
+ }
+
+ return initOptions
+ }
+
+ onMounted(async () => {
+ const { default: Artplayer } = await import(
+ /* webpackChunkName: "artplayer" */ 'artplayer'
+ )
+ const player = new Artplayer(getInitOptions())
+
+ artPlayerInstance = (await props.customPlayer?.(player)) ?? player
+ loaded.value = true
+ resize()
+ })
+
+ onUnmounted(() => {
+ artPlayerInstance?.destroy()
+ })
+
+ return (): (VNode | null)[] => [
+ h('div', {
+ ref: el,
+ class: 'vp-artplayer',
+ style: {
+ width: width.value,
+ height: height.value,
+ },
+ }),
+ loaded.value ? null : h(LoadingIcon),
+ ]
+ },
+})
diff --git a/plugins/features/plugin-media/src/client/components/BiliBili.ts b/plugins/features/plugin-media/src/client/components/BiliBili.ts
new file mode 100644
index 0000000000..993da3bbd5
--- /dev/null
+++ b/plugins/features/plugin-media/src/client/components/BiliBili.ts
@@ -0,0 +1,147 @@
+import { LoadingIcon } from '@vuepress/helper/client'
+import type { VNode } from 'vue'
+import { computed, defineComponent, h, ref } from 'vue'
+
+import { useSize } from '../composables/index.js'
+import { videoIframeAllow } from '../utils/index.js'
+
+import '../styles/bili-bili.css'
+
+const VIDEO_LINK = 'https://player.bilibili.com/player.html'
+
+export const BiliBili = defineComponent({
+ name: 'BiliBili',
+
+ props: {
+ /**
+ * BiliBili video id
+ *
+ * B 站视频 ID
+ */
+ bvid: String,
+
+ /**
+ * BiliBili video aid
+ *
+ * B 站视频 a ID
+ */
+ aid: String,
+
+ /**
+ * BiliBili video cid
+ *
+ * B 站视频 CID
+ */
+ cid: String,
+
+ /**
+ * BiliBili video title
+ *
+ * B 站视频标题
+ */
+ title: {
+ type: String,
+ default: 'A BiliBili video',
+ },
+
+ /**
+ * BiliBili video page
+ *
+ * B 站视频分页
+ */
+ page: {
+ type: [String, Number],
+ default: 1,
+ },
+
+ /**
+ * Component width
+ *
+ * 组件宽度
+ */
+ width: {
+ type: [String, Number],
+ default: '100%',
+ },
+
+ /**
+ * Component height
+ *
+ * 组件高度
+ */
+ height: [String, Number],
+
+ /**
+ * Component width / height ratio
+ *
+ * 组件长宽比
+ */
+ ratio: {
+ type: [String, Number],
+ default: 16 / 9,
+ },
+
+ /**
+ * Start time in seconds
+ *
+ * 基于秒数的开始时间
+ */
+ time: {
+ type: [String, Number],
+ default: 0,
+ },
+
+ /**
+ * Whether autoplay
+ *
+ * 是否自动播放
+ */
+ autoplay: Boolean,
+ },
+
+ setup(props) {
+ const { el, width, height, resize } = useSize(props)
+
+ const loaded = ref(false)
+
+ const videoLink = computed(() => {
+ const { aid, bvid, cid, autoplay, time, page } = props
+
+ return aid && cid
+ ? `${VIDEO_LINK}?aid=${aid}&cid=${cid}&t=${time}&autoplay=${
+ autoplay ? 1 : 0
+ }&p=${page}`
+ : bvid
+ ? `${VIDEO_LINK}?bvid=${bvid}&t=${time}&autoplay=${autoplay ? 1 : 0}`
+ : null
+ })
+
+ return (): (VNode | null)[] =>
+ videoLink.value
+ ? [
+ h(
+ 'div',
+ { class: 'bilibili-desc' },
+ h('a', { class: 'sr-only', href: videoLink.value }, props.title),
+ ),
+ h('iframe', {
+ ref: el,
+ // Tip: `https://www.bilibili.com/blackboard/newplayer.html?bvid=${props.bvid}&as_wide=1&page=1` only support whitelist sites now
+ src: videoLink.value,
+ title: props.title,
+ class: 'bilibili-iframe',
+ allow: videoIframeAllow,
+ style: {
+ width: width.value,
+ height: loaded.value ? height.value : 0,
+ },
+ onLoad: () => {
+ loaded.value = true
+ resize()
+ },
+ }),
+ loaded.value ? null : h(LoadingIcon),
+ ]
+ : []
+ },
+})
diff --git a/plugins/features/plugin-media/src/client/components/VidStack.ts b/plugins/features/plugin-media/src/client/components/VidStack.ts
new file mode 100644
index 0000000000..6269c66459
--- /dev/null
+++ b/plugins/features/plugin-media/src/client/components/VidStack.ts
@@ -0,0 +1,144 @@
+import type { ExactLocaleConfig } from '@vuepress/helper/client'
+import { isArray, isString, useLocaleConfig } from '@vuepress/helper/client'
+import type {
+ DASHNamespaceLoader,
+ DefaultLayoutProps,
+ HLSConstructorLoader,
+ PlayerSrc,
+ TextTrackInit,
+} from 'vidstack'
+import type { MediaPlayerElement } from 'vidstack/elements'
+import type { VidstackPlayerConfig } from 'vidstack/global/player'
+import type { PropType, VNode } from 'vue'
+import { defineComponent, h, onBeforeUnmount, onMounted, shallowRef } from 'vue'
+
+import type { VidstackLocaleData } from '../../shared/index.js'
+import { getLink } from '../utils/getLink.js'
+
+import 'vidstack/player/styles/default/theme.css'
+import 'vidstack/player/styles/default/layouts/audio.css'
+import 'vidstack/player/styles/default/layouts/video.css'
+import '../styles/vidstack.css'
+
+declare const DASHJS_INSTALLED: boolean
+declare const HLS_JS_INSTALLED: boolean
+declare const VIDSTACK_LOCALES: ExactLocaleConfig
+
+export const VidStack = defineComponent({
+ name: 'VidStack',
+
+ props: {
+ /**
+ * sources
+ */
+ src: {
+ type: [String, Array, Object] as PropType,
+ required: true,
+ },
+
+ /**
+ * tracks
+ */
+ tracks: {
+ type: Array as PropType,
+ default: () => [],
+ },
+
+ /**
+ * poster
+ */
+ poster: String,
+
+ /**
+ * thumbnails
+ */
+ thumbnails: String,
+
+ /**
+ * title
+ */
+ title: String,
+
+ /**
+ * VidStack player options
+ */
+ player: {
+ type: Object as PropType<
+ Omit<
+ VidstackPlayerConfig,
+ 'poster' | 'sources' | 'src' | 'target' | 'title' | 'tracks'
+ >
+ >,
+ },
+
+ /**
+ * VidStack layout options
+ */
+ layout: {
+ type: Object as PropType>,
+ },
+
+ /**
+ * Dark mode
+ */
+ darkmode: Boolean,
+ },
+
+ setup(props) {
+ const vidstack = shallowRef()
+ const locale = useLocaleConfig(VIDSTACK_LOCALES)
+
+ let player: MediaPlayerElement | null = null
+
+ onMounted(async () => {
+ if (__VUEPRESS_SSR__) return
+
+ const { VidstackPlayer, VidstackPlayerLayout } = await import(
+ 'vidstack/global/player'
+ )
+
+ const options: VidstackPlayerConfig = {
+ target: vidstack.value!,
+ crossOrigin: true,
+ poster: props.poster,
+ title: props.title,
+ ...props.player,
+ layout: new VidstackPlayerLayout({
+ colorScheme: props.darkmode ? 'dark' : 'light',
+ thumbnails: props.thumbnails,
+ translations: locale.value,
+ ...props.layout,
+ }),
+ }
+
+ options.src = isString(props.src)
+ ? getLink(props.src)
+ : isArray(props.src)
+ ? props.src.map((src) => (isString(src) ? getLink(src) : src))
+ : props.src
+
+ if (props.tracks.length) options.tracks = props.tracks
+
+ player = await VidstackPlayer.create(options)
+
+ player.addEventListener('provider-change', () => {
+ if (player!.provider?.type === 'hls' && HLS_JS_INSTALLED)
+ player!.provider.library = (() =>
+ import(
+ /* webpackChunkName: "hls" */ 'hls.js/dist/hls.min.js'
+ )) as HLSConstructorLoader
+ else if (player!.provider?.type === 'dash' && DASHJS_INSTALLED)
+ player!.provider.library = (() =>
+ import(
+ /* webpackChunkName: "dashjs" */ 'dashjs'
+ )) as DASHNamespaceLoader
+ })
+ })
+
+ onBeforeUnmount(() => {
+ player?.destroy()
+ })
+
+ return (): VNode => h('div', { ref: vidstack })
+ },
+})
diff --git a/plugins/features/plugin-media/src/client/components/index.ts b/plugins/features/plugin-media/src/client/components/index.ts
new file mode 100644
index 0000000000..f6ae2de74b
--- /dev/null
+++ b/plugins/features/plugin-media/src/client/components/index.ts
@@ -0,0 +1,3 @@
+export * from './ArtPlayer.js'
+export * from './BiliBili.js'
+export * from './VidStack.js'
diff --git a/plugins/features/plugin-media/src/client/composables/index.ts b/plugins/features/plugin-media/src/client/composables/index.ts
new file mode 100644
index 0000000000..0fc6125277
--- /dev/null
+++ b/plugins/features/plugin-media/src/client/composables/index.ts
@@ -0,0 +1 @@
+export * from "./useSize.js";
diff --git a/plugins/features/plugin-media/src/client/composables/useSize.ts b/plugins/features/plugin-media/src/client/composables/useSize.ts
new file mode 100644
index 0000000000..c23fc8eef9
--- /dev/null
+++ b/plugins/features/plugin-media/src/client/composables/useSize.ts
@@ -0,0 +1,68 @@
+import { isString } from '@vuepress/helper/client'
+import type { MaybeRef } from '@vueuse/core'
+import { useEventListener } from '@vueuse/core'
+import type { Ref, ShallowRef } from 'vue'
+import { computed, isRef, onMounted, ref, shallowRef, unref, watch } from 'vue'
+
+const getValue = (value: number | string): string =>
+ isString(value) ? value : `${value}px`
+
+export interface SizeOptions {
+ width: number | string | undefined
+ height: number | string | undefined
+ ratio: number | string | undefined
+}
+
+export interface SizeInfo {
+ el: ShallowRef
+ width: Ref
+ height: Ref
+ resize: () => void
+}
+
+export const useSize = (
+ options: SizeOptions,
+ extraHeight: MaybeRef = 0,
+): SizeInfo => {
+ const el = shallowRef()
+ const width = computed(() => getValue(unref(options.width) ?? '100%'))
+ const height = ref('auto')
+
+ const getRadio = (ratio: number | string | undefined): number => {
+ if (isString(ratio)) {
+ const [radioWidth, radioHeight] = ratio.split(':')
+ const parsedRadio = Number(radioWidth) / Number(radioHeight)
+
+ if (!Number.isNaN(parsedRadio)) return parsedRadio
+ }
+
+ return typeof ratio === 'number' ? ratio : 16 / 9
+ }
+
+ const getHeight = (widthValue: number): string => {
+ const heightValue = unref(options.height)
+ const ratio = getRadio(unref(options.ratio))
+
+ return heightValue
+ ? getValue(heightValue)
+ : `${widthValue / ratio + unref(extraHeight)}px`
+ }
+
+ const updateHeight = (): void => {
+ if (el.value) height.value = getHeight(el.value.clientWidth)
+ }
+
+ onMounted(() => {
+ updateHeight()
+ if (isRef(extraHeight)) watch(extraHeight, updateHeight)
+ useEventListener('orientationchange', updateHeight)
+ useEventListener('resize', updateHeight)
+ })
+
+ return {
+ el,
+ width,
+ height,
+ resize: updateHeight,
+ }
+}
diff --git a/plugins/features/plugin-media/src/client/index.ts b/plugins/features/plugin-media/src/client/index.ts
new file mode 100644
index 0000000000..166b13ec0e
--- /dev/null
+++ b/plugins/features/plugin-media/src/client/index.ts
@@ -0,0 +1 @@
+export * from './components/index.js'
diff --git a/plugins/features/plugin-media/src/client/styles/art-player.scss b/plugins/features/plugin-media/src/client/styles/art-player.scss
new file mode 100644
index 0000000000..77f1db9cb3
--- /dev/null
+++ b/plugins/features/plugin-media/src/client/styles/art-player.scss
@@ -0,0 +1,3 @@
+.vp-artplayer {
+ text-align: center;
+}
diff --git a/plugins/features/plugin-media/src/client/styles/bili-bili.scss b/plugins/features/plugin-media/src/client/styles/bili-bili.scss
new file mode 100644
index 0000000000..c3cf8afac0
--- /dev/null
+++ b/plugins/features/plugin-media/src/client/styles/bili-bili.scss
@@ -0,0 +1,15 @@
+.bilibili-desc a {
+ @media print {
+ display: block;
+ }
+}
+
+.bilibili-iframe {
+ margin: 8px 0;
+ border: none;
+ border-radius: 8px;
+
+ @media print {
+ display: none;
+ }
+}
diff --git a/plugins/features/plugin-media/src/client/styles/vidstack.scss b/plugins/features/plugin-media/src/client/styles/vidstack.scss
new file mode 100644
index 0000000000..cf13c8c5b5
--- /dev/null
+++ b/plugins/features/plugin-media/src/client/styles/vidstack.scss
@@ -0,0 +1,15 @@
+.vds-audio-layout,
+.vds-video-layout {
+ box-shadow: 2px 2px 10px 0 var(--vp-c-shadow);
+
+ &,
+ &.light,
+ &.dark {
+ --default-brand: var(--vp-c-accent-bg);
+ --audio-bg: var(--vp-c-bg);
+ --default-controls-color: var(--vp-c-text-mute);
+ --default-focus-ring-color: var(--vp-c-accent-bg);
+ --default-border-radius: 8px;
+ --audio-border: 1px solid var(--vp-c-border);
+ }
+}
diff --git a/plugins/features/plugin-media/src/client/utils/getLink.ts b/plugins/features/plugin-media/src/client/utils/getLink.ts
new file mode 100644
index 0000000000..3e4915bbd9
--- /dev/null
+++ b/plugins/features/plugin-media/src/client/utils/getLink.ts
@@ -0,0 +1,5 @@
+import { isLinkHttp } from "@vuepress/helper/client";
+import { withBase } from "vuepress/client";
+
+export const getLink = (url: string): string =>
+ isLinkHttp(url) ? url : withBase(url);
diff --git a/plugins/features/plugin-media/src/client/utils/iframeAllow.ts b/plugins/features/plugin-media/src/client/utils/iframeAllow.ts
new file mode 100644
index 0000000000..88b9ca1b3d
--- /dev/null
+++ b/plugins/features/plugin-media/src/client/utils/iframeAllow.ts
@@ -0,0 +1,2 @@
+export const videoIframeAllow =
+ "accelerometer; autoplay; clipboard-write; encrypted-media; fullscreen; gyroscope; picture-in-picture";
diff --git a/plugins/features/plugin-media/src/client/utils/index.ts b/plugins/features/plugin-media/src/client/utils/index.ts
new file mode 100644
index 0000000000..7cc76e50fc
--- /dev/null
+++ b/plugins/features/plugin-media/src/client/utils/index.ts
@@ -0,0 +1,3 @@
+export * from './iframeAllow.js'
+export * from './getLink.js'
+export * from './registerMse.js'
diff --git a/plugins/features/plugin-media/src/client/utils/registerMse.ts b/plugins/features/plugin-media/src/client/utils/registerMse.ts
new file mode 100644
index 0000000000..5ee41221f9
--- /dev/null
+++ b/plugins/features/plugin-media/src/client/utils/registerMse.ts
@@ -0,0 +1,97 @@
+declare const DASHJS_INSTALLED: boolean
+declare const HLS_JS_INSTALLED: boolean
+declare const MPEGTS_JS_INSTALLED: boolean
+
+export const SUPPORTED_VIDEO_TYPES = ['mp4', 'mp3', 'webm', 'ogg']
+
+if (typeof DASHJS_INSTALLED !== 'undefined' && DASHJS_INSTALLED)
+ SUPPORTED_VIDEO_TYPES.push('mpd', 'dash')
+
+if (typeof HLS_JS_INSTALLED !== 'undefined' && HLS_JS_INSTALLED)
+ SUPPORTED_VIDEO_TYPES.push('m3u8', 'hls')
+
+if (typeof MPEGTS_JS_INSTALLED !== 'undefined' && MPEGTS_JS_INSTALLED)
+ SUPPORTED_VIDEO_TYPES.push('ts', 'flv')
+
+export const getTypeByUrl = (url: string): string => url.split('.').pop() ?? ''
+
+export const registerMseDash = async (
+ mediaElement: HTMLMediaElement,
+ src: string,
+ onDestroy: (destroy: () => void) => void,
+ autoPlay = false,
+ startTime = 0,
+): Promise => {
+ if (typeof DASHJS_INSTALLED !== 'undefined' && DASHJS_INSTALLED) {
+ const dashjs = (await import(/* webpackChunkName: "dashjs" */ 'dashjs'))
+ .default
+
+ if (dashjs.supportsMediaSource()) {
+ const dashPlayer = dashjs.MediaPlayer().create()
+
+ dashPlayer.initialize(mediaElement, src, autoPlay, startTime)
+
+ onDestroy(() => {
+ dashPlayer.destroy()
+ })
+ }
+ }
+}
+
+export const registerMseFlv = async (
+ mediaElement: HTMLMediaElement,
+ src: string,
+ onDestroy: (destroy: () => void) => void,
+): Promise => {
+ if (typeof MPEGTS_JS_INSTALLED !== 'undefined' && MPEGTS_JS_INSTALLED) {
+ const mpegts = (
+ await import(
+ /* webpackChunkName: "mpegts.js" */ 'mpegts.js/dist/mpegts.js'
+ )
+ ).default
+
+ if (mpegts.isSupported()) {
+ const flvPlayer = mpegts.createPlayer({
+ type: 'flv',
+ url: src,
+ })
+
+ flvPlayer.attachMediaElement(mediaElement)
+ flvPlayer.load()
+
+ onDestroy(() => {
+ flvPlayer.destroy()
+ })
+ }
+ }
+}
+
+export const registerMseHls = async (
+ mediaElement: HTMLMediaElement,
+ src: string,
+ onDestroy: (destroy: () => void) => void,
+): Promise => {
+ if (
+ mediaElement.canPlayType('application/x-mpegURL') ||
+ mediaElement.canPlayType('application/vnd.apple.mpegURL')
+ ) {
+ mediaElement.src = src
+ } else if (typeof HLS_JS_INSTALLED !== 'undefined' && HLS_JS_INSTALLED) {
+ const HLS = (
+ await import(/* webpackChunkName: "hls.js" */ 'hls.js/dist/hls.min.js')
+ ).default
+
+ if (HLS.isSupported()) {
+ const hlsInstance = new HLS()
+
+ hlsInstance.attachMedia(mediaElement)
+ hlsInstance.on(HLS.Events.MEDIA_ATTACHED, () => {
+ hlsInstance.loadSource(src)
+ })
+
+ onDestroy(() => {
+ hlsInstance.destroy()
+ })
+ }
+ }
+}
diff --git a/plugins/features/plugin-media/src/node/getDefine.ts b/plugins/features/plugin-media/src/node/getDefine.ts
new file mode 100644
index 0000000000..f7f1e38be1
--- /dev/null
+++ b/plugins/features/plugin-media/src/node/getDefine.ts
@@ -0,0 +1,52 @@
+import { getFullLocaleConfig } from '@vuepress/helper'
+import type { App } from 'vuepress/core'
+
+import { vidstackLocaleInfo } from './locales/index.js'
+import type { MediaPluginOptions } from './options.js'
+import { isInstalled } from './utils.js'
+
+export const getDefine =
+ (options: MediaPluginOptions): ((app: App) => Record) =>
+ (app) => {
+ const result: Record = {}
+
+ if (options.artplayer || options.vidstack) {
+ result.DASHJS_INSTALLED = isInstalled('dashjs')
+ result.HLS_JS_INSTALLED = isInstalled('hls.js')
+ }
+
+ if (options.artplayer) {
+ result.ART_PLAYER_OPTIONS = {
+ fullscreen: true,
+ playbackRate: true,
+ setting: true,
+ ...componentOptions.artPlayer,
+ }
+ result.MPEGTS_JS_INSTALLED = isInstalled('mpegts.js')
+ }
+
+ if (options.pdf) {
+ result.PDF_LOCALES = getFullLocaleConfig({
+ app,
+ name: 'pdf',
+ default: pdfLocaleInfo,
+ config: locales.pdf,
+ })
+ result.PDFJS_URL =
+ typeof componentOptions.pdf?.pdfjs === 'string'
+ ? componentOptions.pdf.pdfjs
+ : componentOptions.pdf?.pdfjs === false
+ ? null
+ : 'https://theme-hope-assets.vuejs.press/pdfjs/'
+ }
+
+ if (options.vidstack)
+ result.VIDSTACK_LOCALES = getFullLocaleConfig({
+ app,
+ name: 'vidstack',
+ default: vidstackLocaleInfo,
+ config: locales.vidstack,
+ })
+
+ return result
+ }
diff --git a/plugins/features/plugin-media/src/node/index.ts b/plugins/features/plugin-media/src/node/index.ts
new file mode 100644
index 0000000000..334250e5a6
--- /dev/null
+++ b/plugins/features/plugin-media/src/node/index.ts
@@ -0,0 +1,2 @@
+export * from './mediaPlugin.js'
+export type * from './options.js'
diff --git a/plugins/features/plugin-media/src/node/locales/index.ts b/plugins/features/plugin-media/src/node/locales/index.ts
new file mode 100644
index 0000000000..2415b895b9
--- /dev/null
+++ b/plugins/features/plugin-media/src/node/locales/index.ts
@@ -0,0 +1 @@
+export * from './vidstack.js'
diff --git a/plugins/features/plugin-media/src/node/locales/vidstack.ts b/plugins/features/plugin-media/src/node/locales/vidstack.ts
new file mode 100644
index 0000000000..6508677d60
--- /dev/null
+++ b/plugins/features/plugin-media/src/node/locales/vidstack.ts
@@ -0,0 +1,1068 @@
+import type { DefaultLocaleInfo } from '@vuepress/helper'
+
+import type { VidstackLocaleData } from '../../shared/index.js'
+
+export const vidstackLocaleInfo: DefaultLocaleInfo = [
+ [['en', 'en-US'], {}],
+ [
+ ['zh', 'zh-CN', 'zh-Hans'],
+ {
+ 'Play': '播放',
+ 'Pause': '暂停',
+ 'Enter Fullscreen': '进入全屏',
+ 'Exit Fullscreen': '退出全屏',
+ 'Enter PiP': '进入画中画',
+ 'Exit PiP': '退出画中画',
+ 'Closed-Captions On': '开启字幕',
+ 'Closed-Captions Off': '关闭字幕',
+ 'Mute': '静音',
+ 'Volume': '音量',
+ 'Seek Forward': '快进',
+ 'Seek Backward': '快退',
+ 'Announcements': '公告',
+ 'Accessibility': '无障碍',
+ 'Audio': '音频',
+ 'Auto': '自动',
+ 'Boost': '增强',
+ 'Captions': '字幕',
+ 'Caption Styles': '字幕样式',
+ 'Captions look like this': '字幕样式',
+ 'Chapters': '章节',
+ 'Connected': '已连接',
+ 'Continue': '继续',
+ 'Connecting': '连接中',
+ 'Default': '默认',
+ 'Disabled': '已禁用',
+ 'Disconnected': '已断开连接',
+ 'Display Background': '显示背景',
+ 'Download': '下载',
+ 'Font': '字体',
+ 'Family': '字体',
+ 'Fullscreen': '全屏',
+ 'Keyboard Animations': '键盘动画',
+ 'LIVE': '直播',
+ 'Loop': '循环',
+ 'Normal': '正常',
+ 'Off': '关闭',
+ 'Playback': '播放',
+ 'PiP': '画中画',
+ 'Quality': '画质',
+ 'Replay': '重播',
+ 'Reset': '重置',
+ 'Seek': '搜索',
+ 'Settings': '设置',
+ 'Skip To Live': '跳转到直播',
+ 'Speed': '速度',
+ 'Size': '大小',
+ 'Color': '颜色',
+ 'Opacity': '透明度',
+ 'Shadow': '阴影',
+ 'Text': '文本',
+ 'Text Background': '文本背景',
+ 'Track': '轨道',
+ 'Unmute': '取消静音',
+ },
+ ],
+ [
+ ['zh-tw', 'zh-Hant'],
+ {
+ 'Play': '播放',
+ 'Pause': '暫停',
+ 'Enter Fullscreen': '進入全螢幕',
+ 'Exit Fullscreen': '退出全螢幕',
+ 'Enter PiP': '進入畫中畫',
+ 'Exit PiP': '退出畫中畫',
+ 'Closed-Captions On': '開啟字幕',
+ 'Closed-Captions Off': '關閉字幕',
+ 'Mute': '靜音',
+ 'Volume': '音量',
+ 'Seek Forward': '快進',
+ 'Seek Backward': '快退',
+ 'Announcements': '公告',
+ 'Accessibility': '無障礙',
+ 'Audio': '音頻',
+ 'Auto': '自動',
+ 'Boost': '增強',
+ 'Captions': '字幕',
+ 'Caption Styles': '字幕樣式',
+ 'Captions look like this': '字幕樣式',
+ 'Chapters': '章節',
+ 'Connected': '已連接',
+ 'Continue': '繼續',
+ 'Connecting': '連接中',
+ 'Default': '默認',
+ 'Disabled': '已禁用',
+ 'Disconnected': '已斷開連接',
+ 'Display Background': '顯示背景',
+ 'Download': '下載',
+ 'Font': '字體',
+ 'Family': '字體',
+ 'Fullscreen': '全螢幕',
+ 'Keyboard Animations': '鍵盤動畫',
+ 'LIVE': '直播',
+ 'Loop': '循環',
+ 'Normal': '正常',
+ 'Off': '關閉',
+ 'Playback': '播放',
+ 'PiP': '畫中畫',
+ 'Quality': '畫質',
+ 'Replay': '重播',
+ 'Reset': '重置',
+ 'Seek': '搜索',
+ 'Settings': '設置',
+ 'Skip To Live': '跳轉到直播',
+ 'Speed': '速度',
+ 'Size': '大小',
+ 'Color': '顏色',
+ 'Opacity': '透明度',
+ 'Shadow': '陰影',
+ 'Text': '文本',
+ 'Text Background': '文本背景',
+ 'Track': '軌道',
+ 'Unmute': '取消靜音',
+ },
+ ],
+ [
+ ['de'],
+ {
+ 'Play': 'Wiedergabe',
+ 'Pause': 'Pause',
+ 'Enter Fullscreen': 'Vollbildmodus aktivieren',
+ 'Exit Fullscreen': 'Vollbildmodus beenden',
+ 'Enter PiP': 'Bild-in-Bild-Modus aktivieren',
+ 'Exit PiP': 'Bild-in-Bild-Modus beenden',
+ 'Closed-Captions On': 'Untertitel einblenden',
+ 'Closed-Captions Off': 'Untertitel ausblenden',
+ 'Mute': 'Stummschalten',
+ 'Volume': 'Lautstärke',
+ 'Seek Forward': 'Vorspulen',
+ 'Seek Backward': 'Zurückspulen',
+ 'Announcements': 'Ankündigungen',
+ 'Accessibility': 'Barrierefreiheit',
+ 'Audio': 'Audio',
+ 'Auto': 'Automatisch',
+ 'Boost': 'Boost',
+ 'Captions': 'Untertitel',
+ 'Caption Styles': 'Untertitel-Stile',
+ 'Captions look like this': 'Untertitel-Stile',
+ 'Chapters': 'Kapitel',
+ 'Connected': 'Verbunden',
+ 'Continue': 'Weiter',
+ 'Connecting': 'Verbinden',
+ 'Default': 'Standard',
+ 'Disabled': 'Deaktiviert',
+ 'Disconnected': 'Getrennt',
+ 'Display Background': 'Hintergrund anzeigen',
+ 'Download': 'Herunterladen',
+ 'Font': 'Schriftart',
+ 'Family': 'Schriftfamilie',
+ 'Fullscreen': 'Vollbild',
+ 'Keyboard Animations': 'Tastaturanimationen',
+ 'LIVE': 'LIVE',
+ 'Loop': 'Wiederholen',
+ 'Normal': 'Normal',
+ 'Off': 'Aus',
+ 'Playback': 'Wiedergabe',
+ 'PiP': 'Bild-in-Bild',
+ 'Quality': 'Qualität',
+ 'Replay': 'Wiederholen',
+ 'Reset': 'Zurücksetzen',
+ 'Seek': 'Suchen',
+ 'Settings': 'Einstellungen',
+ 'Skip To Live': 'Zum Live-Modus wechseln',
+ 'Speed': 'Geschwindigkeit',
+ 'Size': 'Größe',
+ 'Color': 'Farbe',
+ 'Opacity': 'Deckkraft',
+ 'Shadow': 'Schatten',
+ 'Text': 'Text',
+ 'Text Background': 'Text-Hintergrund',
+ 'Track': 'Spur',
+ 'Unmute': 'Ton einschalten',
+ },
+ ],
+ [
+ ['vi'],
+ {
+ 'Play': 'Phát',
+ 'Pause': 'Tạm dừng',
+ 'Enter Fullscreen': 'Mở toàn màn hình',
+ 'Exit Fullscreen': 'Thoát toàn màn hình',
+ 'Enter PiP': 'Vào chế độ hình trong hình',
+ 'Exit PiP': 'Thoát chế độ hình trong hình',
+ 'Closed-Captions On': 'Bật phụ đề',
+ 'Closed-Captions Off': 'Tắt phụ đề',
+ 'Mute': 'Tắt tiếng',
+ 'Volume': 'Âm lượng',
+ 'Seek Forward': 'Chuyển tiến',
+ 'Seek Backward': 'Chuyển lùi',
+ 'Announcements': 'Thông báo',
+ 'Accessibility': 'Truy cập',
+ 'Audio': 'Âm thanh',
+ 'Auto': 'Tự động',
+ 'Boost': 'Tăng cường',
+ 'Captions': 'Phụ đề',
+ 'Caption Styles': 'Kiểu phụ đề',
+ 'Captions look like this': 'Kiểu phụ đề',
+ 'Chapters': 'Chương',
+ 'Connected': 'Đã kết nối',
+ 'Continue': 'Tiếp tục',
+ 'Connecting': 'Đang kết nối',
+ 'Default': 'Mặc định',
+ 'Disabled': 'Đã tắt',
+ 'Disconnected': 'Đã ngắt kết nối',
+ 'Display Background': 'Hiển thị nền',
+ 'Download': 'Tải xuống',
+ 'Font': 'Phông chữ',
+ 'Family': 'Phông chữ',
+ 'Fullscreen': 'Toàn màn hình',
+ 'Keyboard Animations': 'Hiệu ứng bàn phím',
+ 'LIVE': 'TRỰC TIẾP',
+ 'Loop': 'Lặp lại',
+ 'Normal': 'Bình thường',
+ 'Off': 'Tắt',
+ 'Playback': 'Phát',
+ 'PiP': 'Hình trong hình',
+ 'Quality': 'Chất lượng',
+ 'Replay': 'Phát lại',
+ 'Reset': 'Đặt lại',
+ 'Seek': 'Tìm kiếm',
+ 'Settings': 'Cài đặt',
+ 'Skip To Live': 'Chuyển đến trực tiếp',
+ 'Speed': 'Tốc độ',
+ 'Size': 'Kích thước',
+ 'Color': 'Màu sắc',
+ 'Opacity': 'Độ mờ',
+ 'Shadow': 'Bóng',
+ 'Text': 'Văn bản',
+ 'Text Background': 'Nền văn bản',
+ 'Track': 'Dấu vết',
+ 'Unmute': 'Bật tiếng',
+ },
+ ],
+ [
+ ['uk'],
+ {
+ 'Play': 'Відтворити',
+ 'Pause': 'Пауза',
+ 'Enter Fullscreen': 'Увійти в повноекранний режим',
+ 'Exit Fullscreen': 'Вийти з повноекранного режиму',
+ 'Enter PiP': 'Увійти в режим картинка-в-картинці',
+ 'Exit PiP': 'Вийти з режиму картинка-в-картинці',
+ 'Closed-Captions On': 'Увімкнути субтитри',
+ 'Closed-Captions Off': 'Вимкнути субтитри',
+ 'Mute': 'Вимкнути звук',
+ 'Volume': 'Гучність',
+ 'Seek Forward': 'Прискорити',
+ 'Seek Backward': 'Прокрутити назад',
+ 'Announcements': 'Оголошення',
+ 'Accessibility': 'Доступність',
+ 'Audio': 'Аудіо',
+ 'Auto': 'Автоматично',
+ 'Boost': 'Підвищити',
+ 'Captions': 'Субтитри',
+ 'Caption Styles': 'Стилі субтитрів',
+ 'Captions look like this': 'Стилі субтитрів',
+ 'Chapters': 'Розділи',
+ 'Connected': 'Підключено',
+ 'Continue': 'Продовжити',
+ 'Connecting': 'Підключення',
+ 'Default': 'За замовчуванням',
+ 'Disabled': 'Вимкнено',
+ 'Disconnected': 'Відключено',
+ 'Display Background': 'Показати фон',
+ 'Download': 'Завантажити',
+ 'Font': 'Шрифт',
+ 'Family': 'Сімейство шрифтів',
+ 'Fullscreen': 'Повноекранний режим',
+ 'Keyboard Animations': 'Анімація клавіатури',
+ 'LIVE': 'Наживо',
+ 'Loop': 'Повторювати',
+ 'Normal': 'Звичайний',
+ 'Off': 'Вимкнено',
+ 'Playback': 'Відтворення',
+ 'PiP': 'Картинка-в-картинці',
+ 'Quality': 'Якість',
+ 'Replay': 'Повторити',
+ 'Reset': 'Скинути',
+ 'Seek': 'Пошук',
+ 'Settings': 'Налаштування',
+ 'Skip To Live': 'Перейти до прямого ефіру',
+ 'Speed': 'Швидкість',
+ 'Size': 'Розмір',
+ 'Color': 'Колір',
+ 'Opacity': 'Непрозорість',
+ 'Shadow': 'Тінь',
+ 'Text': 'Текст',
+ 'Text Background': 'Фон тексту',
+ 'Track': 'Трек',
+ 'Unmute': 'Увімкнути звук',
+ },
+ ],
+ [
+ ['ru'],
+ {
+ 'Play': 'Воспроизвести',
+ 'Pause': 'Пауза',
+ 'Enter Fullscreen': 'Войти в полноэкранный режим',
+ 'Exit Fullscreen': 'Выйти из полноэкранного режима',
+ 'Enter PiP': 'Войти в режим картинка в картинке',
+ 'Exit PiP': 'Выйти из режима картинка в картинке',
+ 'Closed-Captions On': 'Включить субтитры',
+ 'Closed-Captions Off': 'Выключить субтитры',
+ 'Mute': 'Выключить звук',
+ 'Volume': 'Громкость',
+ 'Seek Forward': 'Ускорить',
+ 'Seek Backward': 'Прокрутить назад',
+ 'Announcements': 'Объявления',
+ 'Accessibility': 'Доступность',
+ 'Audio': 'Аудио',
+ 'Auto': 'Автоматически',
+ 'Boost': 'Усилить',
+ 'Captions': 'Субтитры',
+ 'Caption Styles': 'Стили субтитров',
+ 'Captions look like this': 'Стили субтитров',
+ 'Chapters': 'Главы',
+ 'Connected': 'Подключено',
+ 'Continue': 'Продолжить',
+ 'Connecting': 'Подключение',
+ 'Default': 'По умолчанию',
+ 'Disabled': 'Отключено',
+ 'Disconnected': 'Отключено',
+ 'Display Background': 'Показать фон',
+ 'Download': 'Скачать',
+ 'Font': 'Шрифт',
+ 'Family': 'Семейство шрифтов',
+ 'Fullscreen': 'Полноэкранный режим',
+ 'Keyboard Animations': 'Анимации клавиатуры',
+ 'LIVE': 'В прямом эфире',
+ 'Loop': 'Повторять',
+ 'Normal': 'Обычный',
+ 'Off': 'Отключено',
+ 'Playback': 'Воспроизведение',
+ 'PiP': 'Картинка в картинке',
+ 'Quality': 'Качество',
+ 'Replay': 'Повторить',
+ 'Reset': 'Сбросить',
+ 'Seek': 'Поиск',
+ 'Settings': 'Настройки',
+ 'Skip To Live': 'Перейти к прямому эфиру',
+ 'Speed': 'Скорость',
+ 'Size': 'Размер',
+ 'Color': 'Цвет',
+ 'Opacity': 'Непрозрачность',
+ 'Shadow': 'Тень',
+ 'Text': 'Текст',
+ 'Text Background': 'Фон текста',
+ 'Track': 'Трек',
+ 'Unmute': 'Включить звук',
+ },
+ ],
+ [
+ ['br'],
+ {
+ 'Play': 'Reproduzir',
+ 'Pause': 'Pausar',
+ 'Enter Fullscreen': 'Entrar em tela cheia',
+ 'Exit Fullscreen': 'Sair da tela cheia',
+ 'Enter PiP': 'Entrar em PiP',
+ 'Exit PiP': 'Sair de PiP',
+ 'Closed-Captions On': 'Ativar legendas',
+ 'Closed-Captions Off': 'Desativar legendas',
+ 'Mute': 'Mudo',
+ 'Volume': 'Volume',
+ 'Seek Forward': 'Avançar',
+ 'Seek Backward': 'Retroceder',
+ 'Announcements': 'Anúncios',
+ 'Accessibility': 'Acessibilidade',
+ 'Audio': 'Áudio',
+ 'Auto': 'Automático',
+ 'Boost': 'Aumentar',
+ 'Captions': 'Legendas',
+ 'Caption Styles': 'Estilos de legenda',
+ 'Captions look like this': 'Estilos de legenda',
+ 'Chapters': 'Capítulos',
+ 'Connected': 'Conectado',
+ 'Continue': 'Continuar',
+ 'Connecting': 'Conectando',
+ 'Default': 'Padrão',
+ 'Disabled': 'Desativado',
+ 'Disconnected': 'Desconectado',
+ 'Display Background': 'Mostrar fundo',
+ 'Download': 'Baixar',
+ 'Font': 'Fonte',
+ 'Family': 'Família',
+ 'Fullscreen': 'Tela cheia',
+ 'Keyboard Animations': 'Animações do teclado',
+ 'LIVE': 'AO VIVO',
+ 'Loop': 'Repetir',
+ 'Normal': 'Normal',
+ 'Off': 'Desligado',
+ 'Playback': 'Reprodução',
+ 'PiP': 'PiP',
+ 'Quality': 'Qualidade',
+ 'Replay': 'Repetir',
+ 'Reset': 'Redefinir',
+ 'Seek': 'Buscar',
+ 'Settings': 'Configurações',
+ 'Skip To Live': 'Pular para ao vivo',
+ 'Speed': 'Velocidade',
+ 'Size': 'Tamanho',
+ 'Color': 'Cor',
+ 'Opacity': 'Opacidade',
+ 'Shadow': 'Sombra',
+ 'Text': 'Texto',
+ 'Text Background': 'Fundo do texto',
+ 'Track': 'Trilha',
+ 'Unmute': 'Ativar som',
+ },
+ ],
+ [
+ ['p'],
+ {
+ 'Play': 'Odtwórz',
+ 'Pause': 'Pauza',
+ 'Enter Fullscreen': 'Wejdź na pełny ekran',
+ 'Exit Fullscreen': 'Wyjdź z pełnego ekranu',
+ 'Enter PiP': 'Wejdź w tryb obraz w obrazie',
+ 'Exit PiP': 'Wyjdź z trybu obraz w obrazie',
+ 'Closed-Captions On': 'Włącz napisy',
+ 'Closed-Captions Off': 'Wyłącz napisy',
+ 'Mute': 'Wycisz',
+ 'Volume': 'Głośność',
+ 'Seek Forward': 'Przewiń do przodu',
+ 'Seek Backward': 'Przewiń do tyłu',
+ 'Announcements': 'Ogłoszenia',
+ 'Accessibility': 'Dostępność',
+ 'Audio': 'Audio',
+ 'Auto': 'Automatycznie',
+ 'Boost': 'Wzmocnij',
+ 'Captions': 'Napisy',
+ 'Caption Styles': 'Style napisów',
+ 'Captions look like this': 'Style napisów',
+ 'Chapters': 'Rozdziały',
+ 'Connected': 'Połączono',
+ 'Continue': 'Kontynuuj',
+ 'Connecting': 'Łączenie',
+ 'Default': 'Domyślne',
+ 'Disabled': 'Wyłączone',
+ 'Disconnected': 'Rozłączono',
+ 'Display Background': 'Pokaż tło',
+ 'Download': 'Pobierz',
+ 'Font': 'Czcionka',
+ 'Family': 'Rodzina',
+ 'Fullscreen': 'Pełny ekran',
+ 'Keyboard Animations': 'Animacje klawiatury',
+ 'LIVE': 'NA ŻYWO',
+ 'Loop': 'Pętla',
+ 'Normal': 'Normalny',
+ 'Off': 'Wyłącz',
+ 'Playback': 'Odtwarzanie',
+ 'PiP': 'Obraz w obrazie',
+ 'Quality': 'Jakość',
+ 'Replay': 'Powtórz',
+ 'Reset': 'Resetuj',
+ 'Seek': 'Szukaj',
+ 'Settings': 'Ustawienia',
+ 'Skip To Live': 'Przejdź na żywo',
+ 'Speed': 'Prędkość',
+ 'Size': 'Rozmiar',
+ 'Color': 'Kolor',
+ 'Opacity': 'Przezroczystość',
+ 'Shadow': 'Cień',
+ 'Text': 'Tekst',
+ 'Text Background': 'Tło tekstu',
+ 'Track': 'Ścieżka',
+ 'Unmute': 'Włącz dźwięk',
+ },
+ ],
+ [
+ ['sk'],
+ {
+ 'Play': 'Prehrať',
+ 'Pause': 'Pauza',
+ 'Enter Fullscreen': 'Prepnúť na celú obrazovku',
+ 'Exit Fullscreen': 'Opustiť celú obrazovku',
+ 'Enter PiP': 'Prepnúť na obraz v obraze',
+ 'Exit PiP': 'Opustiť obraz v obraze',
+ 'Closed-Captions On': 'Zapnúť titulky',
+ 'Closed-Captions Off': 'Vypnúť titulky',
+ 'Mute': 'Ztlmiť',
+ 'Volume': 'Hlasitosť',
+ 'Seek Forward': 'Posunúť vpred',
+ 'Seek Backward': 'Posunúť vzad',
+ 'Announcements': 'Oznámenia',
+ 'Accessibility': 'Prístupnosť',
+ 'Audio': 'Audio',
+ 'Auto': 'Automaticky',
+ 'Boost': 'Zvýšiť',
+ 'Captions': 'Titulky',
+ 'Caption Styles': 'Štýly titulkov',
+ 'Captions look like this': 'Štýly titulkov',
+ 'Chapters': 'Kapitoly',
+ 'Connected': 'Pripojené',
+ 'Continue': 'Pokračovať',
+ 'Connecting': 'Pripájanie',
+ 'Default': 'Predvolené',
+ 'Disabled': 'Zakázané',
+ 'Disconnected': 'Odpojené',
+ 'Display Background': 'Zobraziť pozadie',
+ 'Download': 'Stiahnuť',
+ 'Font': 'Písmo',
+ 'Family': 'Rodina',
+ 'Fullscreen': 'Celá obrazovka',
+ 'Keyboard Animations': 'Klávesové animácie',
+ 'LIVE': 'ŽIVÉ',
+ 'Loop': 'Opakovať',
+ 'Normal': 'Normálne',
+ 'Off': 'Vypnuté',
+ 'Playback': 'Prehrávanie',
+ 'PiP': 'Obraz v obraze',
+ 'Quality': 'Kvalita',
+ 'Replay': 'Zopakovať',
+ 'Reset': 'Obnoviť',
+ 'Seek': 'Hľadať',
+ 'Settings': 'Nastavenia',
+ 'Skip To Live': 'Preskočiť na živý prenos',
+ 'Speed': 'Rýchlosť',
+ 'Size': 'Veľkosť',
+ 'Color': 'Farba',
+ 'Opacity': 'Priehľadnosť',
+ 'Shadow': 'Tieň',
+ 'Text': 'Text',
+ 'Text Background': 'Pozadie textu',
+ 'Track': 'Stopa',
+ 'Unmute': 'Zapnúť zvuk',
+ },
+ ],
+ [
+ ['fr'],
+ {
+ 'Play': 'Lecture',
+ 'Pause': 'Pause',
+ 'Enter Fullscreen': 'Passer en plein écran',
+ 'Exit Fullscreen': 'Quitter le plein écran',
+ 'Enter PiP': "Passer en mode image dans l'image",
+ 'Exit PiP': "Quitter le mode image dans l'image",
+ 'Closed-Captions On': 'Activer les sous-titres',
+ 'Closed-Captions Off': 'Désactiver les sous-titres',
+ 'Mute': 'Muet',
+ 'Volume': 'Volume',
+ 'Seek Forward': 'Avancer',
+ 'Seek Backward': 'Reculer',
+ 'Announcements': 'Annonces',
+ 'Accessibility': 'Accessibilité',
+ 'Audio': 'Audio',
+ 'Auto': 'Automatique',
+ 'Boost': 'Amplifier',
+ 'Captions': 'Sous-titres',
+ 'Caption Styles': 'Styles de sous-titres',
+ 'Captions look like this': 'Styles de sous-titres',
+ 'Chapters': 'Chapitres',
+ 'Connected': 'Connecté',
+ 'Continue': 'Continuer',
+ 'Connecting': 'Connexion',
+ 'Default': 'Par défaut',
+ 'Disabled': 'Désactivé',
+ 'Disconnected': 'Déconnecté',
+ 'Display Background': "Afficher l'arrière-plan",
+ 'Download': 'Télécharger',
+ 'Font': 'Police',
+ 'Family': 'Famille',
+ 'Fullscreen': 'Plein écran',
+ 'Keyboard Animations': 'Animations du clavier',
+ 'LIVE': 'EN DIRECT',
+ 'Loop': 'Boucle',
+ 'Normal': 'Normal',
+ 'Off': 'Désactivé',
+ 'Playback': 'Lecture',
+ 'PiP': "Image dans l'image",
+ 'Quality': 'Qualité',
+ 'Replay': 'Rejouer',
+ 'Reset': 'Réinitialiser',
+ 'Seek': 'Rechercher',
+ 'Settings': 'Paramètres',
+ 'Skip To Live': 'Aller en direct',
+ 'Speed': 'Vitesse',
+ 'Size': 'Taille',
+ 'Color': 'Couleur',
+ 'Opacity': 'Opacité',
+ 'Shadow': 'Ombre',
+ 'Text': 'Texte',
+ 'Text Background': 'Arrière-plan du texte',
+ 'Track': 'Piste',
+ 'Unmute': 'Activer le son',
+ },
+ ],
+ [
+ ['es'],
+ {
+ 'Play': 'Reproducir',
+ 'Pause': 'Pausa',
+ 'Enter Fullscreen': 'Entrar en pantalla completa',
+ 'Exit Fullscreen': 'Salir de pantalla completa',
+ 'Enter PiP': 'Entrar en modo imagen en imagen',
+ 'Exit PiP': 'Salir de modo imagen en imagen',
+ 'Closed-Captions On': 'Activar subtítulos',
+ 'Closed-Captions Off': 'Desactivar subtítulos',
+ 'Mute': 'Silenciar',
+ 'Volume': 'Volumen',
+ 'Seek Forward': 'Avanzar',
+ 'Seek Backward': 'Retroceder',
+ 'Announcements': 'Anuncios',
+ 'Accessibility': 'Accesibilidad',
+ 'Audio': 'Audio',
+ 'Auto': 'Automático',
+ 'Boost': 'Aumentar',
+ 'Captions': 'Subtítulos',
+ 'Caption Styles': 'Estilos de subtítulos',
+ 'Captions look like this': 'Estilos de subtítulos',
+ 'Chapters': 'Capítulos',
+ 'Connected': 'Conectado',
+ 'Continue': 'Continuar',
+ 'Connecting': 'Conectando',
+ 'Default': 'Predeterminado',
+ 'Disabled': 'Desactivado',
+ 'Disconnected': 'Desconectado',
+ 'Display Background': 'Mostrar fondo',
+ 'Download': 'Descargar',
+ 'Font': 'Fuente',
+ 'Family': 'Familia',
+ 'Fullscreen': 'Pantalla completa',
+ 'Keyboard Animations': 'Animaciones de teclado',
+ 'LIVE': 'EN DIRECTO',
+ 'Loop': 'Repetir',
+ 'Normal': 'Normal',
+ 'Off': 'Desactivado',
+ 'Playback': 'Reproducción',
+ 'PiP': 'Imagen en imagen',
+ 'Quality': 'Calidad',
+ 'Replay': 'Repetir',
+ 'Reset': 'Restablecer',
+ 'Seek': 'Buscar',
+ 'Settings': 'Configuración',
+ 'Skip To Live': 'Ir en directo',
+ 'Speed': 'Velocidad',
+ 'Size': 'Tamaño',
+ 'Color': 'Color',
+ 'Opacity': 'Opacidad',
+ 'Shadow': 'Sombra',
+ 'Text': 'Texto',
+ 'Text Background': 'Fondo de texto',
+ 'Track': 'Pista',
+ 'Unmute': 'Activar sonido',
+ },
+ ],
+ [
+ ['ja'],
+ {
+ 'Play': '再生',
+ 'Pause': '一時停止',
+ 'Enter Fullscreen': 'フルスクリーンに入る',
+ 'Exit Fullscreen': 'フルスクリーンを終了',
+ 'Enter PiP': 'PiPモードに入る',
+ 'Exit PiP': 'PiPモードを終了',
+ 'Closed-Captions On': '字幕をオンにする',
+ 'Closed-Captions Off': '字幕をオフにする',
+ 'Mute': 'ミュート',
+ 'Volume': '音量',
+ 'Seek Forward': '早送り',
+ 'Seek Backward': '巻き戻し',
+ 'Announcements': 'お知らせ',
+ 'Accessibility': 'アクセシビリティ',
+ 'Audio': 'オーディオ',
+ 'Auto': '自動',
+ 'Boost': 'ブースト',
+ 'Captions': '字幕',
+ 'Caption Styles': '字幕スタイル',
+ 'Captions look like this': '字幕スタイル',
+ 'Chapters': 'チャプター',
+ 'Connected': '接続済み',
+ 'Continue': '続行',
+ 'Connecting': '接続中',
+ 'Default': 'デフォルト',
+ 'Disabled': '無効',
+ 'Disconnected': '切断済み',
+ 'Display Background': '背景を表示',
+ 'Download': 'ダウンロード',
+ 'Font': 'フォント',
+ 'Family': 'ファミリー',
+ 'Fullscreen': 'フルスクリーン',
+ 'Keyboard Animations': 'キーボードアニメーション',
+ 'LIVE': 'ライブ',
+ 'Loop': 'ループ',
+ 'Normal': '通常',
+ 'Off': 'オフ',
+ 'Playback': '再生',
+ 'PiP': 'PiP',
+ 'Quality': '画質',
+ 'Replay': 'リプレイ',
+ 'Reset': 'リセット',
+ 'Seek': '検索',
+ 'Settings': '設定',
+ 'Skip To Live': 'ライブにスキップ',
+ 'Speed': '速度',
+ 'Size': 'サイズ',
+ 'Color': '色',
+ 'Opacity': '不透明度',
+ 'Shadow': '影',
+ 'Text': 'テキスト',
+ 'Text Background': 'テキストの背景',
+ 'Track': 'トラック',
+ 'Unmute': 'ミュート解除',
+ },
+ ],
+ [
+ ['tr'],
+ {
+ 'Play': 'Oynat',
+ 'Pause': 'Duraklat',
+ 'Enter Fullscreen': 'Tam Ekrana Gir',
+ 'Exit Fullscreen': 'Tam Ekrandan Çık',
+ 'Enter PiP': "PiP'ye Gir",
+ 'Exit PiP': "PiP'den Çık",
+ 'Closed-Captions On': 'Altyazıları Aç',
+ 'Closed-Captions Off': 'Altyazıları Kapat',
+ 'Mute': 'Sessiz',
+ 'Volume': 'Ses',
+ 'Seek Forward': 'İleri Sar',
+ 'Seek Backward': 'Geri Sar',
+ 'Announcements': 'Duyurular',
+ 'Accessibility': 'Erişilebilirlik',
+ 'Audio': 'Ses',
+ 'Auto': 'Otomatik',
+ 'Boost': 'Artır',
+ 'Captions': 'Altyazılar',
+ 'Caption Styles': 'Altyazı Stilleri',
+ 'Captions look like this': 'Altyazı Stilleri',
+ 'Chapters': 'Bölümler',
+ 'Connected': 'Bağlı',
+ 'Continue': 'Devam',
+ 'Connecting': 'Bağlanıyor',
+ 'Default': 'Varsayılan',
+ 'Disabled': 'Devre Dışı',
+ 'Disconnected': 'Bağlantı Kesildi',
+ 'Display Background': 'Arka Planı Göster',
+ 'Download': 'İndir',
+ 'Font': 'Yazı Tipi',
+ 'Family': 'Aile',
+ 'Fullscreen': 'Tam Ekran',
+ 'Keyboard Animations': 'Klavye Animasyonları',
+ 'LIVE': 'CANLI',
+ 'Loop': 'Döngü',
+ 'Normal': 'Normal',
+ 'Off': 'Kapalı',
+ 'Playback': 'Oynatma',
+ 'PiP': 'PiP',
+ 'Quality': 'Kalite',
+ 'Replay': 'Tekrar Oynat',
+ 'Reset': 'Sıfırla',
+ 'Seek': 'Ara',
+ 'Settings': 'Ayarlar',
+ 'Skip To Live': 'Canlıya Geç',
+ 'Speed': 'Hız',
+ 'Size': 'Boyut',
+ 'Color': 'Renk',
+ 'Opacity': 'Opaklık',
+ 'Shadow': 'Gölge',
+ 'Text': 'Metin',
+ 'Text Background': 'Metin Arka Planı',
+ 'Track': 'Parça',
+ 'Unmute': 'Sesi Aç',
+ },
+ ],
+ [
+ ['ko'],
+ {
+ 'Play': '재생',
+ 'Pause': '일시정지',
+ 'Enter Fullscreen': '전체화면으로 들어가기',
+ 'Exit Fullscreen': '전체화면에서 나가기',
+ 'Enter PiP': 'PiP 모드로 들어가기',
+ 'Exit PiP': 'PiP 모드에서 나가기',
+ 'Closed-Captions On': '자막 켜기',
+ 'Closed-Captions Off': '자막 끄기',
+ 'Mute': '음소거',
+ 'Volume': '볼륨',
+ 'Seek Forward': '앞으로 이동',
+ 'Seek Backward': '뒤로 이동',
+ 'Announcements': '공지',
+ 'Accessibility': '접근성',
+ 'Audio': '오디오',
+ 'Auto': '자동',
+ 'Boost': '부스트',
+ 'Captions': '자막',
+ 'Caption Styles': '자막 스타일',
+ 'Captions look like this': '자막 스타일',
+ 'Chapters': '챕터',
+ 'Connected': '연결됨',
+ 'Continue': '계속',
+ 'Connecting': '연결 중',
+ 'Default': '기본값',
+ 'Disabled': '비활성화됨',
+ 'Disconnected': '연결 끊김',
+ 'Display Background': '배경 표시',
+ 'Download': '다운로드',
+ 'Font': '글꼴',
+ 'Family': '가족',
+ 'Fullscreen': '전체화면',
+ 'Keyboard Animations': '키보드 애니메이션',
+ 'LIVE': '실시간',
+ 'Loop': '반복',
+ 'Normal': '보통',
+ 'Off': '끔',
+ 'Playback': '재생',
+ 'Quality': '품질',
+ 'Replay': '다시 보기',
+ 'Reset': '재설정',
+ 'Seek': '검색',
+ 'Settings': '설정',
+ 'Skip To Live': '실시간으로 이동',
+ 'Speed': '속도',
+ 'Size': '크기',
+ 'Color': '색상',
+ 'Opacity': '불투명도',
+ 'Shadow': '그림자',
+ 'Text': '텍스트',
+ 'Text Background': '텍스트 배경',
+ 'Track': '트랙',
+ 'Unmute': '음소거 해제',
+ },
+ ],
+ [
+ ['fi'],
+ {
+ 'Play': 'Toista',
+ 'Pause': 'Tauko',
+ 'Enter Fullscreen': 'Siirry koko näytölle',
+ 'Exit Fullscreen': 'Poistu koko näytöltä',
+ 'Enter PiP': 'Siirry kuva kuvassa -tilaan',
+ 'Exit PiP': 'Poistu kuva kuvassa -tilasta',
+ 'Closed-Captions On': 'Ota tekstitykset käyttöön',
+ 'Closed-Captions Off': 'Poista tekstitykset käytöstä',
+ 'Mute': 'Mykistä',
+ 'Volume': 'Äänenvoimakkuus',
+ 'Seek Forward': 'Siirry eteenpäin',
+ 'Seek Backward': 'Siirry taaksepäin',
+ 'Announcements': 'Ilmoitukset',
+ 'Accessibility': 'Saavutettavuus',
+ 'Audio': 'Ääni',
+ 'Auto': 'Automaattinen',
+ 'Boost': 'Tehosta',
+ 'Captions': 'Tekstitykset',
+ 'Caption Styles': 'Tekstitystyyli',
+ 'Captions look like this': 'Tekstitystyyli',
+ 'Chapters': 'Luvut',
+ 'Connected': 'Yhdistetty',
+ 'Continue': 'Jatka',
+ 'Connecting': 'Yhdistetään',
+ 'Default': 'Oletus',
+ 'Disabled': 'Poistettu käytöstä',
+ 'Disconnected': 'Yhteys katkaistu',
+ 'Display Background': 'Näytä tausta',
+ 'Download': 'Lataa',
+ 'Font': 'Fontti',
+ 'Family': 'Perhe',
+ 'Fullscreen': 'Koko näyttö',
+ 'Keyboard Animations': 'Näppäin animaatiot',
+ 'LIVE': 'LIVE',
+ 'Loop': 'Toista uudelleen',
+ 'Normal': 'Normaali',
+ 'Off': 'Pois',
+ 'Playback': 'Toisto',
+ 'PiP': 'Kuva kuvassa',
+ 'Quality': 'Laatu',
+ 'Replay': 'Toista uudelleen',
+ 'Reset': 'Palauta',
+ 'Seek': 'Hae',
+ 'Settings': 'Asetukset',
+ 'Skip To Live': 'Siirry suorana',
+ 'Speed': 'Nopeus',
+ 'Size': 'Koko',
+ 'Color': 'Väri',
+ 'Opacity': 'Sameus',
+ 'Shadow': 'Varjo',
+ 'Text': 'Teksti',
+ 'Text Background': 'Tekstin tausta',
+ 'Track': 'Raita',
+ 'Unmute': 'Poista mykistys',
+ },
+ ],
+ [
+ ['hu'],
+ {
+ 'Play': 'Lejátszás',
+ 'Pause': 'Szünet',
+ 'Enter Fullscreen': 'Belépés a teljes képernyős módba',
+ 'Exit Fullscreen': 'Kilépés a teljes képernyős módból',
+ 'Enter PiP': 'Belépés a kép a képben módba',
+ 'Exit PiP': 'Kilépés a kép a képben módból',
+ 'Closed-Captions On': 'Feliratok bekapcsolása',
+ 'Closed-Captions Off': 'Feliratok kikapcsolása',
+ 'Mute': 'Némítás',
+ 'Volume': 'Hangerő',
+ 'Seek Forward': 'Előre ugrás',
+ 'Seek Backward': 'Visszalépés',
+ 'Announcements': 'Hirdetések',
+ 'Accessibility': 'Hozzáférhetőség',
+ 'Audio': 'Hang',
+ 'Auto': 'Automatikus',
+ 'Boost': 'Felerősítés',
+ 'Captions': 'Feliratok',
+ 'Caption Styles': 'Felirat stílusok',
+ 'Captions look like this': 'Felirat stílusok',
+ 'Chapters': 'Fejezetek',
+ 'Connected': 'Csatlakoztatva',
+ 'Continue': 'Folytatás',
+ 'Connecting': 'Csatlakozás',
+ 'Default': 'Alapértelmezett',
+ 'Disabled': 'Letiltva',
+ 'Disconnected': 'Nincs kapcsolat',
+ 'Display Background': 'Háttér megjelenítése',
+ 'Download': 'Letöltés',
+ 'Font': 'Betűtípus',
+ 'Family': 'Család',
+ 'Fullscreen': 'Teljes képernyő',
+ 'Keyboard Animations': 'Billentyűzet animációk',
+ 'LIVE': 'ÉLŐ',
+ 'Loop': 'Ismétlés',
+ 'Normal': 'Normál',
+ 'Off': 'Kikapcsolva',
+ 'Playback': 'Lejátszás',
+ 'PiP': 'Kép a képben',
+ 'Quality': 'Minőség',
+ 'Replay': 'Ismétlés',
+ 'Reset': 'Visszaállítás',
+ 'Seek': 'Keresés',
+ 'Settings': 'Beállítások',
+ 'Skip To Live': 'Ugrás az élő adásra',
+ 'Speed': 'Sebesség',
+ 'Size': 'Méret',
+ 'Color': 'Szín',
+ 'Opacity': 'Átlátszatlanság',
+ 'Shadow': 'Árnyék',
+ 'Text': 'Szöveg',
+ 'Text Background': 'Szöveg háttér',
+ 'Track': 'Sáv',
+ 'Unmute': 'Némítás feloldása',
+ },
+ ],
+ [
+ ['id'],
+ {
+ 'Play': 'Putar',
+ 'Pause': 'Jeda',
+ 'Enter Fullscreen': 'Masuk ke Layar Penuh',
+ 'Exit Fullscreen': 'Keluar dari Layar Penuh',
+ 'Enter PiP': 'Masuk ke Mode PiP',
+ 'Exit PiP': 'Keluar dari Mode PiP',
+ 'Closed-Captions On': 'Nyalakan Teks',
+ 'Closed-Captions Off': 'Matikan Teks',
+ 'Mute': 'Bisukan',
+ 'Volume': 'Volume',
+ 'Seek Forward': 'Cari Maju',
+ 'Seek Backward': 'Cari Mundur',
+ 'Announcements': 'Pengumuman',
+ 'Accessibility': 'Aksesibilitas',
+ 'Audio': 'Audio',
+ 'Auto': 'Otomatis',
+ 'Boost': 'Tingkatkan',
+ 'Captions': 'Teks',
+ 'Caption Styles': 'Gaya Teks',
+ 'Captions look like this': 'Gaya Teks',
+ 'Chapters': 'Bab',
+ 'Connected': 'Terhubung',
+ 'Continue': 'Lanjutkan',
+ 'Connecting': 'Menghubungkan',
+ 'Default': 'Default',
+ 'Disabled': 'Nonaktif',
+ 'Disconnected': 'Terputus',
+ 'Display Background': 'Tampilkan Latar Belakang',
+ 'Download': 'Unduh',
+ 'Font': 'Font',
+ 'Family': 'Keluarga',
+ 'Fullscreen': 'Layar Penuh',
+ 'Keyboard Animations': 'Animasi Keyboard',
+ 'LIVE': 'LANGSUNG',
+ 'Loop': 'Ulang',
+ 'Normal': 'Normal',
+ 'Off': 'Mati',
+ 'Playback': 'Putar',
+ 'PiP': 'PiP',
+ 'Quality': 'Kualitas',
+ 'Replay': 'Putar Ulang',
+ 'Reset': 'Atur Ulang',
+ 'Seek': 'Cari',
+ 'Settings': 'Pengaturan',
+ 'Skip To Live': 'Lewati ke Langsung',
+ 'Speed': 'Kecepatan',
+ 'Size': 'Ukuran',
+ 'Color': 'Warna',
+ 'Opacity': 'Ketidakjelasan',
+ 'Shadow': 'Bayangan',
+ 'Text': 'Teks',
+ 'Text Background': 'Latar Belakang Teks',
+ 'Track': 'Lacak',
+ 'Unmute': 'Bunyikan',
+ },
+ ],
+ [
+ ['nl'],
+ {
+ 'Play': 'Afspelen',
+ 'Pause': 'Pauze',
+ 'Enter Fullscreen': 'Volledig scherm',
+ 'Exit Fullscreen': 'Verlaat volledig scherm',
+ 'Enter PiP': 'Beeld-in-beeld',
+ 'Exit PiP': 'Verlaat beeld-in-beeld',
+ 'Closed-Captions On': 'Ondertiteling aan',
+ 'Closed-Captions Off': 'Ondertiteling uit',
+ 'Mute': 'Geluid uit',
+ 'Volume': 'Volume',
+ 'Seek Forward': 'Vooruit spoelen',
+ 'Seek Backward': 'Achteruit spoelen',
+ 'Announcements': 'Mededelingen',
+ 'Accessibility': 'Toegankelijkheid',
+ 'Audio': 'Audio',
+ 'Auto': 'Automatisch',
+ 'Boost': 'Versterken',
+ 'Captions': 'Ondertiteling',
+ 'Caption Styles': 'Ondertitelstijlen',
+ 'Captions look like this': 'Ondertitelstijlen',
+ 'Chapters': 'Hoofdstukken',
+ 'Connected': 'Verbonden',
+ 'Continue': 'Doorgaan',
+ 'Connecting': 'Verbinden',
+ 'Default': 'Standaard',
+ 'Disabled': 'Uitgeschakeld',
+ 'Disconnected': 'Verbroken',
+ 'Display Background': 'Achtergrond weergeven',
+ 'Download': 'Downloaden',
+ 'Font': 'Lettertype',
+ 'Family': 'Familie',
+ 'Fullscreen': 'Volledig scherm',
+ 'Keyboard Animations': 'Toetsenbordanimaties',
+ 'LIVE': 'LIVE',
+ 'Loop': 'Herhalen',
+ 'Normal': 'Normaal',
+ 'Off': 'Uit',
+ 'Playback': 'Afspelen',
+ 'PiP': 'Beeld-in-beeld',
+ 'Quality': 'Kwaliteit',
+ 'Replay': 'Opnieuw afspelen',
+ 'Reset': 'Resetten',
+ 'Seek': 'Zoeken',
+ 'Settings': 'Instellingen',
+ 'Skip To Live': 'Naar live gaan',
+ 'Speed': 'Snelheid',
+ 'Size': 'Grootte',
+ 'Color': 'Kleur',
+ 'Opacity': 'Dekking',
+ 'Shadow': 'Schaduw',
+ 'Text': 'Tekst',
+ 'Text Background': 'Tekstachtergrond',
+ 'Track': 'Track',
+ 'Unmute': 'Geluid aan',
+ },
+ ],
+]
diff --git a/plugins/features/plugin-media/src/node/mediaPlugin.ts b/plugins/features/plugin-media/src/node/mediaPlugin.ts
new file mode 100644
index 0000000000..51f27736ca
--- /dev/null
+++ b/plugins/features/plugin-media/src/node/mediaPlugin.ts
@@ -0,0 +1,13 @@
+import type { Plugin } from 'vuepress/core'
+import { getDirname, path } from 'vuepress/utils'
+import type { MediaPluginOptions } from './options.js'
+
+const __dirname = import.meta.dirname || getDirname(import.meta.url)
+
+export const mediaPlugin = (options: MediaPluginOptions = {}): Plugin => ({
+ name: '@vuepress/plugin-media',
+
+ clientConfigFile: path.resolve(__dirname, '../client/config.js'),
+
+ define: getDefine(options),
+})
diff --git a/plugins/features/plugin-media/src/node/options.ts b/plugins/features/plugin-media/src/node/options.ts
new file mode 100644
index 0000000000..f7d2a7abf2
--- /dev/null
+++ b/plugins/features/plugin-media/src/node/options.ts
@@ -0,0 +1,14 @@
+/**
+ * Options for @vuepress/plugin-media
+ */
+export interface MediaPluginOptions {
+ artplayer?: boolean
+ bilibili?: boolean
+ vidstack?: boolean
+ pdf?:
+ | boolean
+ | {
+ pdfjs?: string | false
+ }
+ pdfLocales?: Record
+}
diff --git a/plugins/features/plugin-media/src/node/utils.ts b/plugins/features/plugin-media/src/node/utils.ts
new file mode 100644
index 0000000000..2d31e281b5
--- /dev/null
+++ b/plugins/features/plugin-media/src/node/utils.ts
@@ -0,0 +1,22 @@
+import { Logger, ensureEndingSlash, isModuleAvailable } from '@vuepress/helper'
+import { getDirname, path } from 'vuepress/utils'
+
+const __dirname = getDirname(import.meta.url)
+
+export const COMPONENT_PKG: Record = {
+ ArtPlayer: ['artplayer'],
+ AudioPlayer: ['vidstack'],
+ VidStack: ['vidstack'],
+ VideoPlayer: ['vidstack'],
+}
+
+export const CLIENT_FOLDER = ensureEndingSlash(
+ path.resolve(__dirname, '../client'),
+)
+
+export const PLUGIN_NAME = 'vuepress-plugin-components'
+
+export const logger = new Logger(PLUGIN_NAME)
+
+export const isInstalled = (pkg: string): boolean =>
+ isModuleAvailable(pkg, import.meta)
diff --git a/plugins/features/plugin-media/src/shared/artplayer.ts b/plugins/features/plugin-media/src/shared/artplayer.ts
new file mode 100644
index 0000000000..88e181e599
--- /dev/null
+++ b/plugins/features/plugin-media/src/shared/artplayer.ts
@@ -0,0 +1,16 @@
+import type { Option as ArtPlayerInitOptions } from 'artplayer'
+
+export type ArtPlayerOptions = Partial<
+ Omit<
+ ArtPlayerInitOptions,
+ | 'container'
+ | 'contextmenu'
+ | 'controls'
+ | 'customType'
+ | 'layers'
+ | 'plugins'
+ | 'settings'
+ | 'type'
+ | 'url'
+ >
+>
diff --git a/plugins/features/plugin-media/src/shared/index.ts b/plugins/features/plugin-media/src/shared/index.ts
new file mode 100644
index 0000000000..003e1f0db5
--- /dev/null
+++ b/plugins/features/plugin-media/src/shared/index.ts
@@ -0,0 +1,3 @@
+export type * from './artplayer.js'
+export type * from './locales.js'
+export type * from './share.js'
diff --git a/plugins/features/plugin-media/src/shared/locales.ts b/plugins/features/plugin-media/src/shared/locales.ts
new file mode 100644
index 0000000000..d4d927df23
--- /dev/null
+++ b/plugins/features/plugin-media/src/shared/locales.ts
@@ -0,0 +1,18 @@
+import type { DefaultLayoutTranslations } from 'vidstack'
+
+export interface PDFLocaleData {
+ /**
+ * PDF hint text
+ *
+ * @description Only used if the browser does not support embedding PDF and no PDFJS URL is provided.
+ * [url] will be replaced by actual PDF link.
+ *
+ * PDF 提示文字
+ *
+ * @description 只有在浏览器不支持嵌入 PDF 且没有提供 PDFJS URL 时才会使用
+ * [url] 会被实际 PDF 链接替换
+ */
+ hint: string
+}
+
+export type VidstackLocaleData = Partial
diff --git a/plugins/features/plugin-media/src/shared/share.ts b/plugins/features/plugin-media/src/shared/share.ts
new file mode 100644
index 0000000000..155c52b9e2
--- /dev/null
+++ b/plugins/features/plugin-media/src/shared/share.ts
@@ -0,0 +1,122 @@
+export type ShareAction = 'navigate' | 'open' | 'popup' | 'qrcode'
+
+export type BuiltInShareService =
+ | 'buffer'
+ | 'douban'
+ | 'email'
+ | 'evernote'
+ | 'facebook'
+ | 'flipboard'
+ | 'line'
+ | 'linkedin'
+ | 'messenger'
+ | 'pinterest'
+ | 'qq'
+ | 'qrcode'
+ | 'qzone'
+ | 'reddit'
+ | 'skype'
+ | 'sms'
+ | 'snapchat'
+ | 'telegram'
+ | 'tumblr'
+ | 'twitter'
+ | 'vk'
+ | 'weibo'
+ | 'whatsapp'
+ | 'wordpress'
+
+export type ShareServiceVariableName =
+ | 'cover'
+ | 'description'
+ | 'excerpt'
+ | 'image'
+ | 'summary'
+ | 'tags'
+ | 'title'
+ | 'twitterUserName'
+ | 'url'
+
+export interface ShareServiceConfig {
+ /**
+ * Share link
+ *
+ * @description You can use `[` and `]` to wrap the variable name, and the variable will be replaced with the value of the page.:
+ *
+ * - `title` will be replaced with the title of the page
+ * - `description` will be replaced with the description of the page
+ * - `url` will be replaced with the url of the page
+ * - `summary` will be replaced with the summary of the page
+ * - `tags` will be replaced with the tags of the page
+ * - `cover` will be replaced with the cover/banner of the page
+ * - `image` will be replaced with the first image of the page
+ *
+ * 分享链接
+ *
+ * @description 你可以使用 `[` 和 `]` 包裹变量名,变量将会被替换为页面的值:
+ *
+ * - `title` 将会被替换为页面的标题
+ * - `description` 将会被替换为页面的描述
+ * - `url` 将会被替换为页面的链接
+ * - `summary` 将会被替换为页面的综述
+ * - `tags` 将会被替换为页面的标签
+ * - `cover` 将会被替换为页面的封面
+ * - `image` 将会被替换为页面的第一张图片
+ */
+ link: string
+
+ /**
+ * Action of share button
+ *
+ * @description
+ * - `open` will open the link in a new tab
+ * - `navigate` will navigate to the link
+ * - `popup` will open a popup window
+ * - `qrcode` will show a QR code with link
+ *
+ * 分享按钮的行为
+ *
+ * @description
+ * - `open` 将会在新标签页打开链接
+ * - `navigate` 将会跳转到链接
+ * - `popup` 将会打开一个弹窗
+ * - `qrcode` 将会显示一个二维码
+ *
+ * @default "popup"
+ */
+ action?: ShareAction
+
+ /**
+ * Theme color of icon
+ *
+ * 图标的主题色
+ *
+ * @default 'currentColor'
+ */
+ color?: string
+
+ /**
+ * Plain icon shape
+ *
+ * 纯色图标的形状
+ */
+ shape: string
+
+ /**
+ * Colorful icon
+ *
+ * 彩色图标
+ */
+ icon?: string
+}
+
+export interface ShareServiceOptions extends ShareServiceConfig {
+ /**
+ * Service name
+ *
+ * 服务名称
+ */
+ name: string
+}
+
+export type ShareService = BuiltInShareService | ShareServiceOptions
diff --git a/plugins/features/plugin-media/src/shims-mse.d.ts b/plugins/features/plugin-media/src/shims-mse.d.ts
new file mode 100644
index 0000000000..4984bde44c
--- /dev/null
+++ b/plugins/features/plugin-media/src/shims-mse.d.ts
@@ -0,0 +1,11 @@
+declare module 'hls.js/dist/hls.min.js' {
+ import HLS from 'hls.js'
+
+ export = HLS
+}
+
+declare module 'mpegts.js/dist/mpegts.js' {
+ import mpegts from 'mpegts.js'
+
+ export = mpegts
+}
diff --git a/plugins/features/plugin-media/tsconfig.build.json b/plugins/features/plugin-media/tsconfig.build.json
new file mode 100644
index 0000000000..f7f7fe795a
--- /dev/null
+++ b/plugins/features/plugin-media/tsconfig.build.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../../../tsconfig.build.json",
+ "compilerOptions": {
+ "rootDir": "./src",
+ "outDir": "./lib",
+ "types": ["vuepress/client-types", "vite/client", "webpack-env"]
+ },
+ "include": ["./src"],
+ "references": [{ "path": "../../../tools/helper/tsconfig.build.json" }]
+}
diff --git a/plugins/features/plugin-medium-zoom/src/node/mediumZoomPlugin.ts b/plugins/features/plugin-medium-zoom/src/node/mediumZoomPlugin.ts
index 2034f98d57..9155276a8c 100644
--- a/plugins/features/plugin-medium-zoom/src/node/mediumZoomPlugin.ts
+++ b/plugins/features/plugin-medium-zoom/src/node/mediumZoomPlugin.ts
@@ -1,6 +1,6 @@
import type { Plugin } from 'vuepress/core'
import { getDirname, path } from 'vuepress/utils'
-import type { MediumZoomPluginOptions } from './options'
+import type { MediumZoomPluginOptions } from './options.js'
const __dirname = import.meta.dirname || getDirname(import.meta.url)
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index db28365f0a..27b58c2b1a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -679,6 +679,36 @@ importers:
specifier: 'catalog:'
version: 2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.93.2)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(@vuepress/bundler-webpack@2.0.0-rc.26(esbuild@0.25.11)(typescript@5.9.3))(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3))
+ plugins/features/plugin-media:
+ dependencies:
+ '@vuepress/helper':
+ specifier: workspace:*
+ version: link:../../../tools/helper
+ '@vueuse/core':
+ specifier: 'catalog:'
+ version: 14.0.0(vue@3.5.22(typescript@5.9.3))
+ artplayer:
+ specifier: ^5.3.0
+ version: 5.3.0
+ dashjs:
+ specifier: 4.7.4
+ version: 4.7.4
+ hls.js:
+ specifier: ^1.6.13
+ version: 1.6.13
+ mpegts.js:
+ specifier: ^1.7.3
+ version: 1.8.0
+ vidstack:
+ specifier: ^1.12.13
+ version: 1.12.13
+ vue:
+ specifier: 'catalog:'
+ version: 3.5.22(typescript@5.9.3)
+ vuepress:
+ specifier: 'catalog:'
+ version: 2.0.0-rc.26(@vuepress/bundler-vite@2.0.0-rc.26(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.93.2)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(@vuepress/bundler-webpack@2.0.0-rc.26(esbuild@0.25.11)(typescript@5.9.3))(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3))
+
plugins/features/plugin-medium-zoom:
dependencies:
'@vuepress/helper':
@@ -2600,6 +2630,9 @@ packages:
'@floating-ui/dom@1.1.1':
resolution: {integrity: sha512-TpIO93+DIujg3g7SykEAGZMDtbJRrmnYRCNYSjJlvIbGhBjRSNTLVbNeDQBrzy9qDgUbiWdc7KA0uZHZ2tJmiw==}
+ '@floating-ui/dom@1.7.4':
+ resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==}
+
'@floating-ui/utils@0.2.10':
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
@@ -4503,6 +4536,9 @@ packages:
peerDependencies:
marked: ^14.1.0
+ artplayer@5.3.0:
+ resolution: {integrity: sha512-yExO39MpEg4P+bxgChxx1eJfiUPE4q1QQRLCmqGhlsj+ANuaoEkR8hF93LdI5ZyrAcIbJkuEndxEiUoKobifDw==}
+
assertion-error@2.0.1:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'}
@@ -4571,6 +4607,15 @@ packages:
batch@0.6.1:
resolution: {integrity: sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==}
+ bcp-47-match@1.0.3:
+ resolution: {integrity: sha512-LggQ4YTdjWQSKELZF5JwchnBa1u0pIQSZf5lSdOHEdbVP55h0qICA/FUp3+W99q0xqxYa1ZQizTUH87gecII5w==}
+
+ bcp-47-normalize@1.1.1:
+ resolution: {integrity: sha512-jWZ1Jdu3cs0EZdfCkS0UE9Gg01PtxnChjEBySeB+Zo6nkqtFfnvtoQQgP1qU1Oo4qgJgxhTI6Sf9y/pZIhPs0A==}
+
+ bcp-47@1.0.8:
+ resolution: {integrity: sha512-Y9y1QNBBtYtv7hcmoX0tR+tUNSFZGZ6OL6vKPObq8BbOhkCoyayF6ogfLTgAli/KuAEbsYHYUNq2AQuY6IuLag==}
+
before-after-hook@4.0.0:
resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==}
@@ -4795,6 +4840,9 @@ packages:
resolution: {integrity: sha512-rtpaCbr164TPPh+zFdkWpCyZuKkjpAzODfaZCf/SVJZzJN+4bHQb/LP3Jzq5/+84um3XXY8r548XiWKSborwVw==}
engines: {node: ^18.17.0 || >=20.5.0}
+ codem-isoboxer@0.3.9:
+ resolution: {integrity: sha512-4XOTqEzBWrGOZaMd+sTED2hLpzfBbiQCf1W6OBGkIHqk1D8uwy8WFLazVbdQwfDpQ+vf39lqTGPa9IhWW0roTA==}
+
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@@ -5264,6 +5312,9 @@ packages:
resolution: {integrity: sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==}
engines: {node: '>=12'}
+ dashjs@4.7.4:
+ resolution: {integrity: sha512-+hldo25QPP3H/NOwqUrvt4uKdMse60/Gsz9AUAnoYfhga8qHWq4nWiojUosOiigbigkDTCAn9ORcvUaKCvmfCA==}
+
data-view-buffer@1.0.2:
resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==}
engines: {node: '>= 0.4'}
@@ -5560,6 +5611,9 @@ packages:
resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==}
engines: {node: '>= 0.4'}
+ es6-promise@4.2.8:
+ resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==}
+
esbuild-loader@4.4.0:
resolution: {integrity: sha512-4J+hXTpTtEdzUNLoY8ReqDNJx2NoldfiljRCiKbeYUuZmVaiJeDqFgyAzz8uOopaekwRoCcqBFyEroGQLFVZ1g==}
peerDependencies:
@@ -5796,6 +5850,9 @@ packages:
fast-content-type-parse@3.0.0:
resolution: {integrity: sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==}
+ fast-deep-equal@2.0.1:
+ resolution: {integrity: sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==}
+
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
@@ -6188,6 +6245,9 @@ packages:
resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
engines: {node: '>=12.0.0'}
+ hls.js@1.6.13:
+ resolution: {integrity: sha512-hNEzjZNHf5bFrUNvdS4/1RjIanuJ6szpWNfTaX5I6WfGynWXGT7K/YQLYtemSvFExzeMdgdE4SsyVLJbd5PcZA==}
+
hookable@5.5.3:
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
@@ -6213,6 +6273,9 @@ packages:
resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==}
engines: {node: '>=12'}
+ html-entities@1.4.0:
+ resolution: {integrity: sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA==}
+
html-escaper@2.0.2:
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
@@ -6345,6 +6408,9 @@ packages:
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
engines: {node: '>= 4'}
+ immediate@3.0.6:
+ resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
+
immutable@5.1.4:
resolution: {integrity: sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==}
@@ -6360,6 +6426,9 @@ packages:
import-meta-resolve@4.2.0:
resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==}
+ imsc@1.1.5:
+ resolution: {integrity: sha512-V8je+CGkcvGhgl2C1GlhqFFiUOIEdwXbXLiu1Fcubvvbo+g9inauqT3l0pNYXGoLPBj3jxtZz9t+wCopMkwadQ==}
+
imurmurhash@0.1.4:
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
engines: {node: '>=0.8.19'}
@@ -6815,6 +6884,9 @@ packages:
resolution: {integrity: sha512-tNcU3cLH7toloAzhOOrBDhjzgbxpyuYvkf+BPPnnJCdc5EIcdJ8JcT+SglvCQKKyZ6m9dVXtCVlJcA6csxKdEA==}
engines: {node: ^20.17.0 || >=22.9.0}
+ lie@3.1.1:
+ resolution: {integrity: sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==}
+
lightningcss-android-arm64@1.30.2:
resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==}
engines: {node: '>= 12.0.0'}
@@ -6898,8 +6970,11 @@ packages:
lit-element@4.2.1:
resolution: {integrity: sha512-WGAWRGzirAgyphK2urmYOV72tlvnxw7YfyLDgQ+OZnM9vQQBQnumQ7jUJe6unEzwGU3ahFOjuz1iz1jjrpCPuw==}
- lit-html@3.3.1:
- resolution: {integrity: sha512-S9hbyDu/vs1qNrithiNyeyv64c9yqiW9l+DBgI18fL+MTvOtWoFR0FWiyq1TxaYef5wNlpEmzlXoBlZEO+WjoA==}
+ lit-html@2.8.0:
+ resolution: {integrity: sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q==}
+
+ lit-html@3.3.0:
+ resolution: {integrity: sha512-RHoswrFAxY2d8Cf2mm4OZ1DgzCoBKUKSPvA1fhtSELxUERq2aQQ2h05pO9j81gS1o7RIRJ+CePLogfyahwmynw==}
lit@3.3.1:
resolution: {integrity: sha512-Ksr/8L3PTapbdXJCk+EJVB78jDodUMaP54gD24W186zGRARvwrsPfS60wae/SSCTCNZVPd1chXqio1qHQmu4NA==}
@@ -6920,6 +6995,9 @@ packages:
resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==}
engines: {node: '>=14'}
+ localforage@1.10.0:
+ resolution: {integrity: sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==}
+
locate-path@5.0.0:
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
engines: {node: '>=8'}
@@ -7143,6 +7221,10 @@ packages:
mdurl@2.0.0:
resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
+ media-captions@1.0.4:
+ resolution: {integrity: sha512-cyDNmuZvvO4H27rcBq2Eudxo9IZRDCOX/I7VEyqbxsEiD2Ei7UYUhG/Sc5fvMZjmathgz3fEK7iAKqvpY+Ux1w==}
+ engines: {node: '>=16'}
+
media-typer@0.3.0:
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
engines: {node: '>= 0.6'}
@@ -7357,6 +7439,9 @@ packages:
mlly@1.8.0:
resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==}
+ mpegts.js@1.8.0:
+ resolution: {integrity: sha512-ZtujqtmTjWgcDDkoOnLvrOKUTO/MKgLHM432zGDI8oPaJ0S+ebPxg1nEpDpLw6I7KmV/GZgUIrfbWi3qqEircg==}
+
ms@2.0.0:
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
@@ -7545,6 +7630,9 @@ packages:
resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==}
hasBin: true
+ option-validator@2.0.6:
+ resolution: {integrity: sha512-tmZDan2LRIRQyhUGvkff68/O0R8UmF+Btmiiz0SmSw2ng3CfPZB9wJlIjHpe/MKUZqyIZkVIXCrwr1tIN+0Dzg==}
+
optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
@@ -8504,6 +8592,9 @@ packages:
engines: {node: '>=14.0.0'}
hasBin: true
+ sax@1.2.1:
+ resolution: {integrity: sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==}
+
sax@1.4.1:
resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==}
@@ -9166,6 +9257,10 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
+ ua-parser-js@1.0.41:
+ resolution: {integrity: sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==}
+ hasBin: true
+
uc.micro@2.1.0:
resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
@@ -9274,6 +9369,10 @@ packages:
resolution: {integrity: sha512-gwXJnPRewT4rT7sBi/IvxKTjsms7jX7QIDLOClApuZwR49SXbrB1z2NLUZ+vDHyqCj/n58OzRRqaW+B8OZi8vg==}
engines: {node: '>=18.12.0'}
+ unplugin@1.16.1:
+ resolution: {integrity: sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==}
+ engines: {node: '>=14.0.0'}
+
unplugin@2.3.10:
resolution: {integrity: sha512-6NCPkv1ClwH+/BGE9QeoTIl09nuiAt0gS28nn1PvYXsGKRwM2TCbFA2QiilmehPDTXIe684k4rZI1yl3A1PCUw==}
engines: {node: '>=18.12.0'}
@@ -9351,6 +9450,10 @@ packages:
vfile@6.0.3:
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
+ vidstack@1.12.13:
+ resolution: {integrity: sha512-vuNeyRmWoH/7EoFVDYjp9nkgcqtCMmal518LDeb78dYKgWb+p6+vtY0AzDhrkBv5q1UiCn+xwmjMmwvSlPLuhQ==}
+ engines: {node: '>=18'}
+
vite@7.1.12:
resolution: {integrity: sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -9576,6 +9679,10 @@ packages:
resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==}
engines: {node: '>=0.8.0'}
+ webworkify-webpack@git+https://git@github.com:xqq/webworkify-webpack.git#24d1e719b4a6cac37a518b2bb10fe124527ef4ef:
+ resolution: {commit: 24d1e719b4a6cac37a518b2bb10fe124527ef4ef, repo: git@github.com:xqq/webworkify-webpack.git, type: git}
+ version: 2.1.5
+
whatwg-encoding@2.0.0:
resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==}
engines: {node: '>=12'}
@@ -10973,6 +11080,11 @@ snapshots:
dependencies:
'@floating-ui/core': 1.7.3
+ '@floating-ui/dom@1.7.4':
+ dependencies:
+ '@floating-ui/core': 1.7.3
+ '@floating-ui/utils': 0.2.10
+
'@floating-ui/utils@0.2.10': {}
'@gera2ld/jsx-dom@2.2.2':
@@ -13272,6 +13384,10 @@ snapshots:
dependencies:
marked: 16.4.1
+ artplayer@5.3.0:
+ dependencies:
+ option-validator: 2.0.6
+
assertion-error@2.0.1: {}
astral-regex@2.0.0: {}
@@ -13336,6 +13452,19 @@ snapshots:
batch@0.6.1: {}
+ bcp-47-match@1.0.3: {}
+
+ bcp-47-normalize@1.1.1:
+ dependencies:
+ bcp-47: 1.0.8
+ bcp-47-match: 1.0.3
+
+ bcp-47@1.0.8:
+ dependencies:
+ is-alphabetical: 1.0.4
+ is-alphanumerical: 1.0.4
+ is-decimal: 1.0.4
+
before-after-hook@4.0.0: {}
big.js@5.2.2: {}
@@ -13620,6 +13749,8 @@ snapshots:
cmd-shim@7.0.0: {}
+ codem-isoboxer@0.3.9: {}
+
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
@@ -14136,6 +14267,19 @@ snapshots:
dargs@8.1.0: {}
+ dashjs@4.7.4:
+ dependencies:
+ bcp-47-match: 1.0.3
+ bcp-47-normalize: 1.1.1
+ codem-isoboxer: 0.3.9
+ es6-promise: 4.2.8
+ fast-deep-equal: 2.0.1
+ html-entities: 1.4.0
+ imsc: 1.1.5
+ localforage: 1.10.0
+ path-browserify: 1.0.1
+ ua-parser-js: 1.0.41
+
data-view-buffer@1.0.2:
dependencies:
call-bound: 1.0.4
@@ -14448,6 +14592,8 @@ snapshots:
is-date-object: 1.1.0
is-symbol: 1.1.1
+ es6-promise@4.2.8: {}
+
esbuild-loader@4.4.0(webpack@5.102.1(esbuild@0.25.11)):
dependencies:
esbuild: 0.25.11
@@ -14776,6 +14922,8 @@ snapshots:
fast-content-type-parse@3.0.0: {}
+ fast-deep-equal@2.0.1: {}
+
fast-deep-equal@3.1.3: {}
fast-glob@3.3.3:
@@ -15211,6 +15359,8 @@ snapshots:
highlight.js@11.11.1: {}
+ hls.js@1.6.13: {}
+
hookable@5.5.3: {}
hookified@1.12.2: {}
@@ -15238,6 +15388,8 @@ snapshots:
dependencies:
whatwg-encoding: 2.0.0
+ html-entities@1.4.0: {}
+
html-escaper@2.0.2: {}
html-minifier-terser@6.1.0:
@@ -15400,6 +15552,8 @@ snapshots:
ignore@7.0.5: {}
+ immediate@3.0.6: {}
+
immutable@5.1.4: {}
import-fresh@3.3.1:
@@ -15414,6 +15568,10 @@ snapshots:
import-meta-resolve@4.2.0: {}
+ imsc@1.1.5:
+ dependencies:
+ sax: 1.2.1
+
imurmurhash@0.1.4: {}
index-to-position@1.2.0: {}
@@ -15835,6 +15993,10 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ lie@3.1.1:
+ dependencies:
+ immediate: 3.0.6
+
lightningcss-android-arm64@1.30.2:
optional: true
@@ -15896,9 +16058,13 @@ snapshots:
dependencies:
'@lit-labs/ssr-dom-shim': 1.4.0
'@lit/reactive-element': 2.1.1
- lit-html: 3.3.1
+ lit-html: 3.3.0
+
+ lit-html@2.8.0:
+ dependencies:
+ '@types/trusted-types': 2.0.7
- lit-html@3.3.1:
+ lit-html@3.3.0:
dependencies:
'@types/trusted-types': 2.0.7
@@ -15906,7 +16072,7 @@ snapshots:
dependencies:
'@lit/reactive-element': 2.1.1
lit-element: 4.2.1
- lit-html: 3.3.1
+ lit-html: 3.3.0
load-json-file@7.0.1: {}
@@ -15924,6 +16090,10 @@ snapshots:
pkg-types: 2.3.0
quansync: 0.2.11
+ localforage@1.10.0:
+ dependencies:
+ lie: 3.1.1
+
locate-path@5.0.0:
dependencies:
p-locate: 4.1.0
@@ -16248,6 +16418,8 @@ snapshots:
mdurl@2.0.0: {}
+ media-captions@1.0.4: {}
+
media-typer@0.3.0: {}
medium-zoom@1.1.0: {}
@@ -16545,6 +16717,11 @@ snapshots:
pkg-types: 1.3.1
ufo: 1.6.1
+ mpegts.js@1.8.0:
+ dependencies:
+ es6-promise: 4.2.8
+ webworkify-webpack: git+https://git@github.com:xqq/webworkify-webpack.git#24d1e719b4a6cac37a518b2bb10fe124527ef4ef
+
ms@2.0.0: {}
ms@2.1.3: {}
@@ -16734,6 +16911,10 @@ snapshots:
opener@1.5.2: {}
+ option-validator@2.0.6:
+ dependencies:
+ kind-of: 6.0.3
+
optionator@0.9.4:
dependencies:
deep-is: 0.1.4
@@ -17695,6 +17876,8 @@ snapshots:
optionalDependencies:
'@parcel/watcher': 2.5.1
+ sax@1.2.1: {}
+
sax@1.4.1: {}
schema-utils@4.3.3:
@@ -18475,6 +18658,8 @@ snapshots:
typescript@5.9.3: {}
+ ua-parser-js@1.0.41: {}
+
uc.micro@2.1.0: {}
ufo@1.6.1: {}
@@ -18582,6 +18767,11 @@ snapshots:
pathe: 2.0.3
picomatch: 4.0.3
+ unplugin@1.16.1:
+ dependencies:
+ acorn: 8.15.0
+ webpack-virtual-modules: 0.6.2
+
unplugin@2.3.10:
dependencies:
'@jridgewell/remapping': 2.3.5
@@ -18671,6 +18861,13 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.3
+ vidstack@1.12.13:
+ dependencies:
+ '@floating-ui/dom': 1.7.4
+ lit-html: 2.8.0
+ media-captions: 1.0.4
+ unplugin: 1.16.1
+
vite@7.1.12(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(sass-embedded@1.93.2)(sass@1.93.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1):
dependencies:
esbuild: 0.25.11
@@ -18932,6 +19129,8 @@ snapshots:
websocket-extensions@0.1.4: {}
+ webworkify-webpack@git+https://git@github.com:xqq/webworkify-webpack.git#24d1e719b4a6cac37a518b2bb10fe124527ef4ef: {}
+
whatwg-encoding@2.0.0:
dependencies:
iconv-lite: 0.6.3
diff --git a/tsconfig.build.json b/tsconfig.build.json
index 04148c01aa..9125d6fcb9 100644
--- a/tsconfig.build.json
+++ b/tsconfig.build.json
@@ -80,6 +80,7 @@
{ "path": "./plugins/features/plugin-copy-code/tsconfig.build.json" },
{ "path": "./plugins/features/plugin-copyright/tsconfig.build.json" },
{ "path": "./plugins/features/plugin-icon/tsconfig.build.json" },
+ { "path": "./plugins/features/plugin-media/tsconfig.build.json" },
{ "path": "./plugins/features/plugin-medium-zoom/tsconfig.build.json" },
{ "path": "./plugins/features/plugin-notice/tsconfig.build.json" },
{ "path": "./plugins/features/plugin-nprogress/tsconfig.build.json" },