summaryrefslogtreecommitdiff
path: root/src/components/MotionArtwork.svelte
blob: 646df26f644aae5ab3c993035dc2caee90355213 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
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>