diff --git a/spellcheck/dictionary.txt b/spellcheck/dictionary.txt index 7f88b2f437f6..a7d63598f188 100644 --- a/spellcheck/dictionary.txt +++ b/spellcheck/dictionary.txt @@ -1021,3 +1021,6 @@ yospace-connector-web Yottabyte Youbora Zettabyte +HLG +HLG-compatible +UIs diff --git a/src/components/DocCardListSearch/index.tsx b/src/components/DocCardListSearch/index.tsx new file mode 100644 index 000000000000..6777d694164e --- /dev/null +++ b/src/components/DocCardListSearch/index.tsx @@ -0,0 +1,109 @@ +import { Fragment, useMemo, useState, type JSX } from 'react'; +import DocCardList, { type Props as DocCardListProps } from '@theme/DocCardList'; +import { useDocsVersion } from '@docusaurus/plugin-content-docs/client'; +import { PropSidebarItem, PropSidebarItemCategory } from '@docusaurus/plugin-content-docs'; +import Heading, { type HeadingType } from '@theme/Heading'; +import styles from './styles.module.css'; + +export interface DocCardGroup { + label?: string; + items: PropSidebarItem[]; +} + +export function isCategory(item: PropSidebarItem): item is PropSidebarItemCategory { + return item.type === 'category'; +} + +function itemText(item: PropSidebarItem, groupLabel: string, description?: string): string { + const label = item.type === 'link' || item.type === 'category' ? item.label : ''; + return `${label} ${description ?? ''} ${groupLabel}`.toLowerCase(); +} + +// Filters groups by whitespace-separated terms (all must match) against +// item label + doc description + group label. +// Category items match on their label + group label. Empty groups are dropped. +// With an empty query, returns groups unchanged. +export function useDocCardListSearch(groups: DocCardGroup[]): { + query: string; + setQuery: (q: string) => void; + filteredGroups: DocCardGroup[]; + noMatches: boolean; +} { + const { docs } = useDocsVersion(); + const [query, setQuery] = useState(''); + + const filteredGroups = useMemo(() => { + const terms = query.trim().toLowerCase().split(/\s+/).filter(Boolean); + if (terms.length === 0) return groups; + return groups + .map((group) => ({ + ...group, + items: group.items.filter((item) => { + const haystack = + item.type === 'link' + ? itemText(item, group.label ?? '', item.docId ? docs[item.docId]?.description : undefined) + : item.type === 'category' + ? itemText(item, group.label ?? '') + : ''; + return terms.every((term) => haystack.includes(term)); + }), + })) + .filter((group) => group.items.length > 0); + }, [query, groups, docs]); + + return { query, setQuery, filteredGroups, noMatches: query.trim() !== '' && filteredGroups.length === 0 }; +} + +export function DocCardListSearchInput({ + query, + onQueryChange, + placeholder = 'Search…', +}: { + query: string; + onQueryChange: (q: string) => void; + placeholder?: string; +}): JSX.Element { + return ( + onQueryChange(event.target.value)} + onKeyDown={(event) => { + if (event.key === 'Escape') onQueryChange(''); + }} + /> + ); +} + +export interface SearchableDocCardGroupsProps extends Omit { + groups: DocCardGroup[]; + headingLevel?: HeadingType; + searchable?: boolean; + searchPlaceholder?: string; +} + +export default function SearchableDocCardGroups({ + groups, + headingLevel, + searchable = false, + searchPlaceholder = 'Search…', + ...props +}: SearchableDocCardGroupsProps): JSX.Element { + const { query, setQuery, filteredGroups, noMatches } = useDocCardListSearch(groups); + + return ( + <> + {searchable && } + {noMatches &&

No results match “{query.trim()}”.

} + {filteredGroups.map((group, index) => ( + + {group.label && headingLevel && {group.label}} + + + ))} + + ); +} diff --git a/src/components/DocCardListSearch/styles.module.css b/src/components/DocCardListSearch/styles.module.css new file mode 100644 index 000000000000..c5bee95164c2 --- /dev/null +++ b/src/components/DocCardListSearch/styles.module.css @@ -0,0 +1,12 @@ +.searchInput { + display: block; + width: 100%; + max-width: 32rem; + margin-bottom: 1.5rem; + padding: 0.5rem 0.75rem; + font: inherit; + color: var(--ifm-font-color-base); + background: var(--ifm-background-surface-color); + border: 1px solid var(--ifm-color-emphasis-300); + border-radius: var(--ifm-global-radius); +} diff --git a/src/components/SidebarCategoryDocCardList/index.tsx b/src/components/SidebarCategoryDocCardList/index.tsx index 1c99cd87e7d8..6dcd8b56f65f 100644 --- a/src/components/SidebarCategoryDocCardList/index.tsx +++ b/src/components/SidebarCategoryDocCardList/index.tsx @@ -1,24 +1,22 @@ -import { Fragment } from 'react'; -import DocCardList, { type Props as DocCardListProps } from '@theme/DocCardList'; +import type { JSX } from 'react'; +import type { Props as DocCardListProps } from '@theme/DocCardList'; import { useDocsSidebar } from '@docusaurus/plugin-content-docs/client'; -import { PropSidebarItem, PropSidebarItemCategory } from '@docusaurus/plugin-content-docs'; -import Heading, { type HeadingType } from '@theme/Heading'; +import type { HeadingType } from '@theme/Heading'; +import SearchableDocCardGroups, { isCategory } from '../DocCardListSearch'; export interface Props extends Omit { headingLevel?: HeadingType; + searchable?: boolean; + searchPlaceholder?: string; } -function isCategory(item: PropSidebarItem): item is PropSidebarItemCategory { - return item.type === 'category'; -} - -export default function SidebarCategoryDocCardList({ headingLevel, ...props }: Props) { +export default function SidebarCategoryDocCardList({ headingLevel = 'h2', searchable = false, searchPlaceholder, ...props }: Props): JSX.Element { const sidebar = useDocsSidebar(); - const categories = sidebar?.items.filter(isCategory) ?? []; - return categories.map((category) => ( - - {category.label} - - - )); + const groups = (sidebar?.items.filter(isCategory) ?? []).map((category) => ({ + label: category.label, + items: category.items, + })); + return ( + + ); } diff --git a/src/components/SidebarDocCardList/index.tsx b/src/components/SidebarDocCardList/index.tsx index 0cc8e124b40e..9cd6106227be 100644 --- a/src/components/SidebarDocCardList/index.tsx +++ b/src/components/SidebarDocCardList/index.tsx @@ -1,31 +1,23 @@ -import { Fragment } from 'react'; +import type { JSX } from 'react'; import { useDoc, useDocsSidebar } from '@docusaurus/plugin-content-docs/client'; -import DocCardList, { type Props as DocCardListProps } from '@theme/DocCardList'; -import { PropSidebarItem, PropSidebarItemCategory } from '@docusaurus/plugin-content-docs'; -import Heading, { type HeadingType } from '@theme/Heading'; +import type { Props as DocCardListProps } from '@theme/DocCardList'; +import type { HeadingType } from '@theme/Heading'; +import SearchableDocCardGroups, { isCategory } from '../DocCardListSearch'; export interface Props extends Omit { headingLevel?: HeadingType; + searchable?: boolean; + searchPlaceholder?: string; } -function isCategory(item: PropSidebarItem): item is PropSidebarItemCategory { - return item.type === 'category'; -} - -export default function SidebarDocCardList({ headingLevel, ...props }: Props) { +export default function SidebarDocCardList({ headingLevel, searchable = false, searchPlaceholder, ...props }: Props): JSX.Element { const doc = useDoc(); const sidebar = useDocsSidebar(); const items = (sidebar?.items ?? []) // Hide the current doc page from list .filter((item) => !(item.type === 'link' && item.docId === doc.metadata.id)); - if (headingLevel) { - const subcategories = items.filter(isCategory); - return subcategories.map((subcategory) => ( - - {subcategory.label} - - - )); - } - return ; + const groups = headingLevel ? items.filter(isCategory).map((subcategory) => ({ label: subcategory.label, items: subcategory.items })) : [{ items }]; + return ( + + ); } diff --git a/src/components/SubcategoryDocCardList/index.tsx b/src/components/SubcategoryDocCardList/index.tsx index dbf71b120427..e6663a108767 100644 --- a/src/components/SubcategoryDocCardList/index.tsx +++ b/src/components/SubcategoryDocCardList/index.tsx @@ -1,24 +1,22 @@ -import { Fragment } from 'react'; -import DocCardList, { type Props as DocCardListProps } from '@theme/DocCardList'; +import type { JSX } from 'react'; +import type { Props as DocCardListProps } from '@theme/DocCardList'; import { useCurrentSidebarCategory } from '@docusaurus/plugin-content-docs/client'; -import { PropSidebarItem, PropSidebarItemCategory } from '@docusaurus/plugin-content-docs'; -import Heading, { type HeadingType } from '@theme/Heading'; +import type { HeadingType } from '@theme/Heading'; +import SearchableDocCardGroups, { isCategory } from '../DocCardListSearch'; export interface Props extends Omit { headingLevel?: HeadingType; + searchable?: boolean; + searchPlaceholder?: string; } -function isCategory(item: PropSidebarItem): item is PropSidebarItemCategory { - return item.type === 'category'; -} - -export default function SubcategoryDocCardList({ headingLevel, ...props }: Props) { +export default function SubcategoryDocCardList({ headingLevel = 'h2', searchable = false, searchPlaceholder, ...props }: Props): JSX.Element { const category = useCurrentSidebarCategory(); - const subcategories = category.items.filter(isCategory); - return subcategories.map((subcategory) => ( - - {subcategory.label} - - - )); + const groups = category.items.filter(isCategory).map((subcategory) => ({ + label: subcategory.label, + items: subcategory.items, + })); + return ( + + ); } diff --git a/theoplayer/examples/_components/ConvivaDemo.tsx b/theoplayer/examples/_components/ConvivaDemo.tsx new file mode 100644 index 000000000000..67605a5851a9 --- /dev/null +++ b/theoplayer/examples/_components/ConvivaDemo.tsx @@ -0,0 +1,161 @@ +import React, { type JSX, useEffect, useRef, useState } from 'react'; +import useBaseUrl from '@docusaurus/useBaseUrl'; +import Example, { type ExampleController } from '@site/src/components/Example'; +import styles from '../shared.module.css'; + +interface StreamTemplate { + id: string; + label: string; + assetName: string; + streamType: 'VOD' | 'LIVE'; + source: { + sources: { src: string; type: string }; + poster?: string; + metadata: { title: string; subtitle: string }; + }; +} + +interface EventEntry { + time: string; + event: string; + details: string; +} + +const DEFAULT_CUSTOMER_KEY = '876a2328cc34e791190d855daf389567c96d1e86'; +const DEFAULT_GATEWAY_URL = `https://${DEFAULT_CUSTOMER_KEY}.ts-testonly.conviva.com`; + +const templates: StreamTemplate[] = [ + { + id: 'hls-vod', + label: 'HLS VOD (Big Buck Bunny)', + assetName: 'Big Buck Bunny (HLS)', + streamType: 'VOD', + source: { + sources: { src: 'https://cdn.theoplayer.com/video/big_buck_bunny/big_buck_bunny_corrected.m3u8', type: 'application/x-mpegurl' }, + poster: 'https://cdn.theoplayer.com/video/big_buck_bunny/poster.jpg', + metadata: { title: 'Big Buck Bunny', subtitle: 'HLS • VOD' }, + }, + }, + { + id: 'hls-live', + label: 'HLS live (Unified Streaming)', + assetName: 'Live stream (HLS)', + streamType: 'LIVE', + source: { + sources: { src: 'https://demo.unified-streaming.com/k8s/live/stable/live.isml/.m3u8', type: 'application/x-mpegurl' }, + metadata: { title: 'Live stream', subtitle: 'HLS • LIVE' }, + }, + }, + { + id: 'dash-vod', + label: 'DASH VOD (Big Buck Bunny)', + assetName: 'Big Buck Bunny (DASH)', + streamType: 'VOD', + source: { + sources: { src: 'https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd', type: 'application/dash+xml' }, + poster: 'https://cdn.theoplayer.com/video/big_buck_bunny/poster.jpg', + metadata: { title: 'Big Buck Bunny', subtitle: 'DASH • VOD' }, + }, + }, + { + id: 'dash-live', + label: 'DASH live (Unified Streaming)', + assetName: 'Live stream (DASH)', + streamType: 'LIVE', + source: { + sources: { src: 'https://demo.unified-streaming.com/k8s/live/stable/live.isml/.mpd', type: 'application/dash+xml' }, + metadata: { title: 'Live stream', subtitle: 'DASH • LIVE' }, + }, + }, +]; + +export default function ConvivaDemo(): JSX.Element { + const exampleRef = useRef(null); + const logRef = useRef(null); + const [templateId, setTemplateId] = useState(templates[0].id); + const [customerKey, setCustomerKey] = useState(''); + const [gatewayUrl, setGatewayUrl] = useState(''); + const [events, setEvents] = useState([]); + + useEffect(() => { + if (logRef.current) { + logRef.current.scrollTop = logRef.current.scrollHeight; + } + }, [events]); + + function loadStream(): void { + const template = templates.find((entry) => entry.id === templateId); + if (!template) return; + exampleRef.current?.postMessage({ + type: 'load-stream', + template: { assetName: template.assetName, streamType: template.streamType, source: template.source }, + // An empty input falls back to its placeholder value. + customerKey: customerKey.trim() || DEFAULT_CUSTOMER_KEY, + gatewayUrl: gatewayUrl.trim() || DEFAULT_GATEWAY_URL, + }); + } + + return ( + <> + { + if (message.type === 'conviva-events' && Array.isArray(message.events)) { + setEvents(message.events as EventEntry[]); + } + }} + /> +
+
+ + + + +
+
+ {events.length === 0 ? ( + No events reported yet. + ) : ( + events.map((entry, index) => ( +
+ {entry.time} + {entry.event} + {entry.details} +
+ )) + )} +
+
+ + ); +} diff --git a/theoplayer/examples/_components/DetectionDemo.tsx b/theoplayer/examples/_components/DetectionDemo.tsx new file mode 100644 index 000000000000..2c34945eff1a --- /dev/null +++ b/theoplayer/examples/_components/DetectionDemo.tsx @@ -0,0 +1,98 @@ +import React, { type JSX, useRef, useState } from 'react'; +import useBaseUrl from '@docusaurus/useBaseUrl'; +import Example, { type ExampleController } from '@site/src/components/Example'; +import styles from '../shared.module.css'; + +type CheckStatus = 'supported' | 'unsupported' | 'unavailable'; + +interface Check { + label: string; + status: CheckStatus; +} + +interface SourceOption { + label: string; + source: unknown; +} + +interface Props { + src: string; + sources?: SourceOption[]; +} + +const alertClass = { + success: 'alert--success', + warning: 'alert--warning', + danger: 'alert--danger', + unknown: 'alert--info', +} as const; + +const checkIcon: Record = { + supported: '✔', + unsupported: '✘', + unavailable: '—', +}; + +export default function DetectionDemo({ src, sources }: Props): JSX.Element { + const exampleRef = useRef(null); + const [level, setLevel] = useState('unknown'); + const [summary, setSummary] = useState('Checking support…'); + const [checks, setChecks] = useState([]); + const [codecs, setCodecs] = useState(''); + const [activeSource, setActiveSource] = useState(0); + + return ( + <> +
+ {summary} + {checks.length > 0 && ( +
    + {checks.map((check) => ( +
  • + {checkIcon[check.status]} {check.label} +
  • + ))} +
+ )} + {codecs &&
Now playing: {codecs}
} +
+ { + if (message.type === 'detection-status') { + const nextLevel = + typeof message.level === 'string' && message.level in alertClass ? (message.level as keyof typeof alertClass) : 'unknown'; + setLevel(nextLevel); + if (typeof message.summary === 'string') setSummary(message.summary); + if (Array.isArray(message.checks)) setChecks(message.checks as Check[]); + } else if (message.type === 'playing-codecs' && typeof message.codecs === 'string') { + setCodecs(message.codecs); + } + }} + /> + {sources && sources.length > 1 && ( +
+
+ Source +
+ {sources.map((option, index) => ( + + ))} +
+
+
+ )} + + ); +} diff --git a/theoplayer/examples/_components/TheoLiveDemo.tsx b/theoplayer/examples/_components/TheoLiveDemo.tsx new file mode 100644 index 000000000000..39a4a4cfc741 --- /dev/null +++ b/theoplayer/examples/_components/TheoLiveDemo.tsx @@ -0,0 +1,31 @@ +import React, { type JSX, useState } from 'react'; +import useBaseUrl from '@docusaurus/useBaseUrl'; +import Example from '@site/src/components/Example'; +import styles from '../shared.module.css'; + +export default function TheoLiveDemo(): JSX.Element { + const [latency, setLatency] = useState(null); + + return ( + <> + { + if (message.type === 'latency') { + setLatency(typeof message.latency === 'number' ? message.latency : null); + } + }} + /> +
+ + + + + + + +
player.latency.currentLatency{latency == null ? '—' : `${latency.toFixed(2)} s`}
+
+ + ); +} diff --git a/theoplayer/examples/analytics/01-conviva.mdx b/theoplayer/examples/analytics/01-conviva.mdx new file mode 100644 index 000000000000..a7b6a6132571 --- /dev/null +++ b/theoplayer/examples/analytics/01-conviva.mdx @@ -0,0 +1,55 @@ +--- +sidebar_custom_props: { icon: '📊' } +description: Monitor playback with the Conviva connector. +--- + +# Conviva + +import CodeBlock from '@theme/CodeBlock'; +import demoSource from '!!raw-loader!../../static/theoplayer/v11/examples/conviva/demo.html'; +import ConvivaDemo from '../_components/ConvivaDemo'; + + + +This example reports playback to [Conviva](https://pulse.conviva.com/) using the +[Conviva connector for THEOplayer](https://www.npmjs.com/package/@theoplayer/conviva-connector-web) +([API reference](https://theoplayer.github.io/web-connectors/api/modules/Conviva_Connector.html)). +The connector bridges player events to the Conviva SDK: it starts a session when playback begins +and reports content metadata, buffering and errors automatically. +The connector must be created before any source is set on the player. + +```js +const convivaIntegration = new THEOplayerConvivaConnector.ConvivaConnector(player, convivaMetadata, { + debug: true, + customerKey, + gatewayUrl, +}); + +// Source-specific metadata does not carry over to the next source, +// so set it again after every sourcechange. +player.addEventListener('sourcechange', () => { + convivaIntegration.setContentInfo({ + ['Conviva.assetName']: 'Big Buck Bunny', + ['Conviva.streamUrl']: source.sources.src, + ['Conviva.streamType']: 'VOD', + }); +}); +``` + +The Customer key and Gateway URL inputs accept your own credentials; an empty input falls back to +the demo's test values (omit `gatewayUrl` for production). Pressing **Load stream** calls +`convivaIntegration.destroy()` to close the previous session, creates a new `ConvivaConnector` +with the entered credentials, and then sets the selected source. +Use the stream templates to switch between HLS/DASH and VOD/live sources; the content metadata is +updated on each `sourcechange`. + +:::note +With `debug: true`, Conviva SDK debug logs are printed in the browser's DevTools console. +::: + +
+ Code + + {demoSource} + +
diff --git a/theoplayer/examples/analytics/_category_.json b/theoplayer/examples/analytics/_category_.json new file mode 100644 index 000000000000..df97ffdb0aef --- /dev/null +++ b/theoplayer/examples/analytics/_category_.json @@ -0,0 +1,7 @@ +{ + "label": "Analytics", + "position": 9, + "customProps": { + "icon": "📊" + } +} diff --git a/theoplayer/examples/index.mdx b/theoplayer/examples/index.mdx index 3779fa4807b6..2d68f7fb6070 100644 --- a/theoplayer/examples/index.mdx +++ b/theoplayer/examples/index.mdx @@ -11,4 +11,4 @@ See the player in action! import SubcategoryDocCardList from '@site/src/components/SubcategoryDocCardList'; - + diff --git a/theoplayer/examples/shared.module.css b/theoplayer/examples/shared.module.css index 02992e4810ea..202b60b1f629 100644 --- a/theoplayer/examples/shared.module.css +++ b/theoplayer/examples/shared.module.css @@ -44,6 +44,22 @@ flex-wrap: wrap; } +.controlsColumn { + display: flex; + flex-direction: column; + align-items: stretch; + gap: 0.75rem; + margin-bottom: 0.75rem; +} + +.controlsColumn select { + width: 100%; +} + +.controlsColumn > :last-child { + align-self: flex-start; +} + .formGrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); @@ -271,16 +287,19 @@ } .eventLogRow { - display: flex; - flex-wrap: wrap; + display: grid; + grid-template-columns: auto auto 1fr; gap: 0 0.75rem; } .eventLogRow > :first-child { - flex-shrink: 0; opacity: 0.7; } +.eventLogRow > :last-child { + overflow-wrap: anywhere; +} + .eventLogEmpty { opacity: 0.7; } @@ -413,3 +432,8 @@ width: 25%; } } + +.detectionChecks { + margin: 0.25rem 0 0; + padding-left: 1.25rem; +} diff --git a/theoplayer/examples/streaming/08-millicast.mdx b/theoplayer/examples/streaming/08-millicast.mdx new file mode 100644 index 000000000000..7ad7e98c7343 --- /dev/null +++ b/theoplayer/examples/streaming/08-millicast.mdx @@ -0,0 +1,38 @@ +--- +sidebar_custom_props: { icon: '📡' } +description: Play a sub-second latency OptiView Real-time Streaming live stream. +--- + +# OptiView Real-time Streaming + +import CodeBlock from '@theme/CodeBlock'; +import demoSource from '!!raw-loader!../../static/theoplayer/v11/examples/millicast/demo.html'; +import useBaseUrl from '@docusaurus/useBaseUrl'; +import Example from '@site/src/components/Example'; + + + +[OptiView Real-time Streaming](pathname:///millicast/getting-started/) (formerly Millicast) delivers real-time streaming with sub-second latency over WebRTC. +THEOplayer can subscribe to an OptiView Real-time stream directly: set the source `type` to `"millicast"` and +provide the stream name (`src`) and account identifier (`streamAccountId`). + +```js +player.source = { + sources: { + type: 'millicast', + src: 'multiview', + streamAccountId: 'k9Mwad', + // subscriberToken: '...' // for secure streams + }, +}; +``` + +See the [`MillicastSource`](pathname:///theoplayer/v11/api-reference/web/interfaces/MillicastSource.html) +API reference for all available fields, including `subscriberToken` for secure streams. + +
+ Code + + {demoSource} + +
diff --git a/theoplayer/examples/streaming/09-theolive.mdx b/theoplayer/examples/streaming/09-theolive.mdx new file mode 100644 index 000000000000..8dd8bc8ac261 --- /dev/null +++ b/theoplayer/examples/streaming/09-theolive.mdx @@ -0,0 +1,36 @@ +--- +sidebar_custom_props: { icon: '📺' } +description: Play an OptiView Live Streaming real-time stream and see the current latency. +--- + +# OptiView Live Streaming + +import CodeBlock from '@theme/CodeBlock'; +import demoSource from '!!raw-loader!../../static/theoplayer/v11/examples/theolive/demo.html'; +import TheoLiveDemo from '../_components/TheoLiveDemo'; + + + +[OptiView Live Streaming](../../how-to-guides/web/theolive/getting-started.mdx) delivers real-time video at sub-second latency. +Set the source `type` to `"theolive"` and `src` to your channel or distribution ID. + +```js +player.source = { + sources: { + type: 'theolive', + src: 'ar5c53uzm3si4h4zgkzrju44h', + }, +}; +``` + +The readout above shows `player.latency.currentLatency`, the end-to-end latency in seconds, +updated on every `timeupdate` event. +See the [`TheoLiveSource`](pathname:///theoplayer/v11/api-reference/web/interfaces/TheoLiveSource.html) +API reference for all available fields. + +
+ Code + + {demoSource} + +
diff --git a/theoplayer/examples/streaming/10-dolby-atmos.mdx b/theoplayer/examples/streaming/10-dolby-atmos.mdx new file mode 100644 index 000000000000..4ee90bd2fe26 --- /dev/null +++ b/theoplayer/examples/streaming/10-dolby-atmos.mdx @@ -0,0 +1,63 @@ +--- +sidebar_custom_props: { icon: '🔊' } +description: Detect and play Dolby Atmos audio. +--- + +# Dolby Atmos + +import CodeBlock from '@theme/CodeBlock'; +import demoSource from '!!raw-loader!../../static/theoplayer/v11/examples/dolby-atmos/demo.html'; +import DetectionDemo from '../_components/DetectionDemo'; + + + +This example plays a Dolby Atmos (ec-3, 16/JOC) stream and probes the browser for Atmos support with +[`MediaCapabilities.decodingInfo`](https://developer.mozilla.org/en-US/docs/Web/API/Media_Capabilities_API): +once for plain Dolby Digital Plus decoding, and once with `spatialRendering: true` for Atmos. +When `navigator.mediaCapabilities` is unavailable it falls back to `MediaSource.isTypeSupported`. + +```js +await navigator.mediaCapabilities.decodingInfo({ + type: 'media-source', + audio: { + contentType: 'audio/mp4; codecs="ec-3"', + channels: '16', + bitrate: 768000, + samplerate: 48000, + spatialRendering: true, + }, +}); +``` + +The status box above the player is green when both checks pass, yellow when the stream plays but +without Atmos spatial rendering, and red when the browser cannot decode ec-3 at all. + +:::note +Browsers cannot report whether the connected speakers or headphones are Atmos-capable — +the check only covers decoding, not the output device. +::: + +
+ Code + + {demoSource} + +
diff --git a/theoplayer/examples/streaming/11-dolby-vision.mdx b/theoplayer/examples/streaming/11-dolby-vision.mdx new file mode 100644 index 000000000000..b249b8237fe8 --- /dev/null +++ b/theoplayer/examples/streaming/11-dolby-vision.mdx @@ -0,0 +1,65 @@ +--- +sidebar_custom_props: { icon: '🌈' } +description: Detect and play Dolby Vision video. +--- + +# Dolby Vision + +import CodeBlock from '@theme/CodeBlock'; +import demoSource from '!!raw-loader!../../static/theoplayer/v11/examples/dolby-vision/demo.html'; +import DetectionDemo from '../_components/DetectionDemo'; + + + +This example plays a Dolby Vision profile 8.4 stream (an HLG-compatible base layer with a Dolby Vision +enhancement layer, signaled via supplemental codecs: `dvh1.08.xx` in DASH via `scte214:supplementalCodecs`, +and `dvhe.08.xx` in HLS via `SUPPLEMENTAL-CODECS`). It probes the browser with +[`MediaCapabilities.decodingInfo`](https://developer.mozilla.org/en-US/docs/Web/API/Media_Capabilities_API) +for the Dolby Vision codec (`dvh1.08.09` or `dvhe.08.09`) and the HEVC Main10 HLG base layer +(`hvc1.2.4.L153.90` or `hev1.2.4.L153.90`), and checks for an HDR display with +`matchMedia('(dynamic-range: high)')`, re-checking when the display capability changes. + +```js +await navigator.mediaCapabilities.decodingInfo({ + type: 'media-source', + video: { + contentType: 'video/mp4; codecs="dvh1.08.09"', + bitrate: 18000000, + width: 3840, + height: 2160, + framerate: 60, + transferFunction: 'hlg', + }, +}); + +const hdrDisplay = matchMedia('(dynamic-range: high)').matches; +``` + +The status box is green when Dolby Vision decoding and an HDR display are both present, yellow when the +browser can decode only the HEVC HLG base layer and not Dolby Vision, or when the display does not +support HDR, and red when the browser cannot decode the HEVC base layer at all. + +
+ Code + + {demoSource} + +
diff --git a/theoplayer/examples/ui/07-audio-only.mdx b/theoplayer/examples/ui/07-audio-only.mdx new file mode 100644 index 000000000000..ff7d2f8f5266 --- /dev/null +++ b/theoplayer/examples/ui/07-audio-only.mdx @@ -0,0 +1,61 @@ +--- +sidebar_custom_props: { icon: '🎧' } +description: Build an audio-only player with Open Video UI. +--- + +# Audio-only player + +import CodeBlock from '@theme/CodeBlock'; +import demoSource from '!!raw-loader!../../static/theoplayer/v11/examples/audio-only/demo.html'; +import useBaseUrl from '@docusaurus/useBaseUrl'; +import Example from '@site/src/components/Example'; + + + +With Open Video UI you compose a player from only the controls you need. This example uses a bare +`` element with a track info area in the top chrome and a control bar containing just +a play button, the current time, a seek bar, the remaining time, a mute button and a volume slider — no video area, +center play button or fullscreen button. + +The video is hidden by styling the `media-layer` shadow part with +[`::part`](https://developer.mozilla.org/en-US/docs/Web/CSS/::part), and the aspect-ratio box is removed +with a CSS custom property, so no `display: none` overrides of individual controls are needed: + +```css +theoplayer-ui { + --theoplayer-aspect-ratio: none; + --theoplayer-height: 7.25rem; +} + +theoplayer-ui::part(media-layer) { + visibility: hidden; +} +``` + +```html + +
...
+ + + + +
+
+ + + +
+
+``` + +`no-auto-hide` on the controls keeps them visible at all times instead of letting them +fade out when the player is idle, since there is no video to tap when the controls fade out. +The empty element in the seek bar's `preview` slot replaces the default time preview that pops up +while hovering or dragging the seek bar. + +
+ Code + + {demoSource} + +
diff --git a/theoplayer/examples/ui/08-keep-control-bar-visible.mdx b/theoplayer/examples/ui/08-keep-control-bar-visible.mdx new file mode 100644 index 000000000000..932b5c27d1a4 --- /dev/null +++ b/theoplayer/examples/ui/08-keep-control-bar-visible.mdx @@ -0,0 +1,51 @@ +--- +sidebar_custom_props: { icon: '👀' } +description: Keep the control bar visible at all times. +--- + +# Keep the control bar visible + +import CodeBlock from '@theme/CodeBlock'; +import demoSource from '!!raw-loader!../../static/theoplayer/v11/examples/keep-control-bar-visible/demo.html'; +import defaultUiSource from '!!raw-loader!../../static/theoplayer/v11/examples/keep-control-bar-visible/default-ui.html'; +import useBaseUrl from '@docusaurus/useBaseUrl'; +import Example from '@site/src/components/Example'; + +By default the control bar fades out after a few seconds of inactivity. These examples keep it visible at all times. + +## Open Video UI + + + +Set `user-idle-timeout` to a negative value to disable idle detection, so the controls never fade out. + +```html +... +``` + +
+ Code + + {demoSource} + +
+ +## Default UI + + + +In the [Default UI](../../ui/web/default-ui/index.mdx), override the fade-out transition with two CSS lines. + +```css +.theo-secondary-color.vjs-control-bar { + opacity: 1 !important; + visibility: visible !important; +} +``` + +
+ Code + + {defaultUiSource} + +
diff --git a/theoplayer/static/theoplayer/v11/examples/audio-only/demo.html b/theoplayer/static/theoplayer/v11/examples/audio-only/demo.html new file mode 100644 index 000000000000..1a50b2cd837b --- /dev/null +++ b/theoplayer/static/theoplayer/v11/examples/audio-only/demo.html @@ -0,0 +1,118 @@ + + + + + Audio-only player + + + + + + + + + + + + +
+ +
+
Elephants Dream
+
Blender Foundation
+
+
+ + + + + +
+
+ + + +
+
+ + + diff --git a/theoplayer/static/theoplayer/v11/examples/conviva/demo.html b/theoplayer/static/theoplayer/v11/examples/conviva/demo.html new file mode 100644 index 000000000000..58681c643594 --- /dev/null +++ b/theoplayer/static/theoplayer/v11/examples/conviva/demo.html @@ -0,0 +1,161 @@ + + + + + Conviva + + + + + + + + + + + + + + + + + + + + diff --git a/theoplayer/static/theoplayer/v11/examples/dolby-atmos/demo.html b/theoplayer/static/theoplayer/v11/examples/dolby-atmos/demo.html new file mode 100644 index 000000000000..140fa42d7d27 --- /dev/null +++ b/theoplayer/static/theoplayer/v11/examples/dolby-atmos/demo.html @@ -0,0 +1,106 @@ + + + + + Dolby Atmos + + + + + + + + + + + + + + + + diff --git a/theoplayer/static/theoplayer/v11/examples/dolby-vision/demo.html b/theoplayer/static/theoplayer/v11/examples/dolby-vision/demo.html new file mode 100644 index 000000000000..334b086f9bb5 --- /dev/null +++ b/theoplayer/static/theoplayer/v11/examples/dolby-vision/demo.html @@ -0,0 +1,150 @@ + + + + + Dolby Vision + + + + + + + + + + + + + + + + diff --git a/theoplayer/static/theoplayer/v11/examples/keep-control-bar-visible/default-ui.html b/theoplayer/static/theoplayer/v11/examples/keep-control-bar-visible/default-ui.html new file mode 100644 index 000000000000..35010fe35d40 --- /dev/null +++ b/theoplayer/static/theoplayer/v11/examples/keep-control-bar-visible/default-ui.html @@ -0,0 +1,43 @@ + + + + + Keep control bar visible (Default UI) + + + + + + +
+ + + diff --git a/theoplayer/static/theoplayer/v11/examples/keep-control-bar-visible/demo.html b/theoplayer/static/theoplayer/v11/examples/keep-control-bar-visible/demo.html new file mode 100644 index 000000000000..ee93207710ff --- /dev/null +++ b/theoplayer/static/theoplayer/v11/examples/keep-control-bar-visible/demo.html @@ -0,0 +1,50 @@ + + + + + Keep control bar visible + + + + + + + + + + + + + + + diff --git a/theoplayer/static/theoplayer/v11/examples/millicast/demo.html b/theoplayer/static/theoplayer/v11/examples/millicast/demo.html new file mode 100644 index 000000000000..756e611e3f0b --- /dev/null +++ b/theoplayer/static/theoplayer/v11/examples/millicast/demo.html @@ -0,0 +1,49 @@ + + + + + OptiView Real-time Streaming + + + + + + + + + + + + + + + diff --git a/theoplayer/static/theoplayer/v11/examples/theolive/demo.html b/theoplayer/static/theoplayer/v11/examples/theolive/demo.html new file mode 100644 index 000000000000..a4030aca7370 --- /dev/null +++ b/theoplayer/static/theoplayer/v11/examples/theolive/demo.html @@ -0,0 +1,56 @@ + + + + + OptiView Live Streaming + + + + + + + + + + + + + + + + diff --git a/theoplayer/ui/web/keep-control-bar-visible.mdx b/theoplayer/ui/web/keep-control-bar-visible.mdx index 0bf81a0cf987..2ec1ae96d463 100644 --- a/theoplayer/ui/web/keep-control-bar-visible.mdx +++ b/theoplayer/ui/web/keep-control-bar-visible.mdx @@ -7,12 +7,27 @@ sidebar_label: Keep the control bar visible The question above is typically asked by developers who want to be able to see the control bar at all times without having to continuously move the cursor over it (or tap the screen). -## Implementation +## Open Video UI -The following CSS line is aimed at overwriting the opacity transition and have the control bar always visible. +Set the `user-idle-timeout` attribute (or the `userIdleTimeout` property) to a negative value. This disables idle detection, so the control bar never fades out. + +```html + +``` + +```js +ui.userIdleTimeout = -1; +``` + +## Default UI + +The following CSS lines overwrite the fade-out transition and keep the control bar always visible. ```css .theo-secondary-color.vjs-control-bar { opacity: 1 !important; + visibility: visible !important; } ``` + +See the [Keep the control bar visible](../../examples/ui/08-keep-control-bar-visible.mdx) example for a working demo of both UIs.