summaryrefslogtreecommitdiff
path: root/src/components/MotionArtwork.svelte
diff options
context:
space:
mode:
authorrxliuli <rxliuli@gmail.com>2025-11-04 05:03:50 +0800
committerrxliuli <rxliuli@gmail.com>2025-11-04 05:03:50 +0800
commitbce557cc2dc767628bed6aac87301a1be7c5431b (patch)
treeb51a051228d01fe3306cd7626d4a96768aadb944 /src/components/MotionArtwork.svelte
init commit
Diffstat (limited to 'src/components/MotionArtwork.svelte')
-rw-r--r--src/components/MotionArtwork.svelte152
1 files changed, 152 insertions, 0 deletions
diff --git a/src/components/MotionArtwork.svelte b/src/components/MotionArtwork.svelte
new file mode 100644
index 0000000..646df26
--- /dev/null
+++ b/src/components/MotionArtwork.svelte
@@ -0,0 +1,152 @@
+<script lang="ts">
+ import { createEventDispatcher, onMount, onDestroy } from 'svelte';
+ import { loggerFor } from '@amp/web-apps-logger';
+
+ const logger = loggerFor('components/MotionArtwork');
+
+ type HLSError = {
+ type: string;
+ message: string;
+ details: string;
+ fatal: boolean;
+ handled: boolean;
+ };
+
+ type MotionArtworkError = {
+ type: string;
+ reason: string;
+ fatal: boolean;
+ error?: Error;
+ };
+
+ /** HTML `id` attribute for the <video /> element */
+ export let id: string;
+
+ /** Source URL for the video, an HLS playlist ending in .m3u8 */
+ export let src: string;
+
+ /** Poster image to show while the video is loading */
+ export let poster: string | undefined;
+
+ /** If the video should loop from end to start. */
+ export let loop: boolean = true;
+
+ /** If the audio should be muted on the video. */
+ export let muted: boolean = true;
+
+ /** If the video should be paused when initially loaded. */
+ export let paused: boolean = true;
+
+ /** The constructor to use for creating an Hls playback session. */
+ export let HLS: Window['Hls'] = window.Hls;
+
+ /** RTCReportingAgent instance for RTC reporting on video playback. */
+ export let reportingAgent: any = undefined;
+
+ /** HTMLVideoElement used by HLS.js to render the video */
+ export let videoElement: HTMLVideoElement | null = null;
+
+ /** Internal error state for the component */
+ let errorState: MotionArtworkError | undefined;
+
+ let hlsSession: Window['Hls'] | undefined;
+
+ /** Dispatcher for errors. */
+ const dispatch = createEventDispatcher<{ error: MotionArtworkError }>();
+
+ function handleError(details: MotionArtworkError) {
+ logger.error(
+ `Error playing MotionArtwork with HLS: ${details?.reason}`,
+ details?.error,
+ );
+
+ errorState = {
+ type: details.type,
+ reason: details.reason,
+ fatal: details.fatal,
+ error: details?.error,
+ };
+
+ dispatch('error', errorState);
+ }
+
+ const hlsSupported = HLS?.isSupported() ?? false;
+
+ onMount(function () {
+ if (!hlsSupported) {
+ handleError({
+ type: 'runtime',
+ reason: 'unsupported',
+ fatal: true,
+ });
+ return;
+ }
+
+ // Create a new HLS.js playback session
+ hlsSession = new HLS({
+ debug: false,
+ debugLevel: 'error',
+ enablePerformanceLogging: false,
+ nativeControlsEnabled: false,
+
+ appData: {
+ reportingAgent: reportingAgent,
+ serviceName: reportingAgent?.ServiceName,
+ },
+ });
+
+ hlsSession.on(
+ HLS.Events.ERROR,
+ function (_event: string, error: HLSError) {
+ handleError({
+ type: 'hls',
+ reason: error.message,
+ fatal: error.fatal,
+ error: error as unknown as Error,
+ });
+ },
+ );
+
+ // Direct HLS.js to the VideoElement to use and start loading the video source
+ hlsSession.attachMedia(videoElement);
+ hlsSession.loadSource(src, {
+ /* HLS.js loading options go here */
+ });
+ });
+
+ onDestroy(() => {
+ // Stop the video, release resources, and destroy the HLS context
+ hlsSession?.destroy();
+ });
+</script>
+
+{#if errorState !== undefined}
+ <slot name="error" error={errorState} {poster} />
+{:else}
+ <!-- svelte-ignore a11y-media-has-caption -->
+ <video
+ {id}
+ {loop}
+ {poster}
+ preload="none"
+ data-loop={true}
+ playsinline={true}
+ controls={false}
+ bind:this={videoElement}
+ bind:muted
+ bind:paused
+ on:play
+ on:ended
+ on:loadedmetadata
+ />
+{/if}
+
+<style>
+ video {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ object-position: center center;
+ aspect-ratio: var(--aspect-ratio);
+ }
+</style>