diff --git a/dotcom-rendering/.storybook/decorators/themeDecorator.tsx b/dotcom-rendering/.storybook/decorators/themeDecorator.tsx index 1eb110abac2..b1f534ba8ab 100644 --- a/dotcom-rendering/.storybook/decorators/themeDecorator.tsx +++ b/dotcom-rendering/.storybook/decorators/themeDecorator.tsx @@ -57,8 +57,9 @@ export const colourSchemeDecorator = (formats: ArticleFormat[]): Decorator => (Story, context) => ( <> - {formats.map((format) => ( + {formats.map((format, index) => (
; + +export default meta; + +type Story = StoryObj; + +export const AudioPlayer = { + args: { + // src: audioFile, + src: 'https://audio.guim.co.uk/2024/10/18-57753-USEE_181024.mp3', + mediaId: 'mediaId', + showVolumeControls: true, + }, + parameters: { + // We only want to snapshot the `multipleFormats` version below. + chromatic: { disable: true }, + }, +} satisfies Story; + +export const MultipleFormats = { + args: AudioPlayer.args, + parameters: { + formats: defaultFormats, + chromatic: { + modes: { + horizontal: allModes.splitHorizontal, + }, + }, + }, +} satisfies Story; diff --git a/dotcom-rendering/src/components/AudioPlayer/AudioPlayer.tsx b/dotcom-rendering/src/components/AudioPlayer/AudioPlayer.tsx new file mode 100644 index 00000000000..eb1fdf6a49a --- /dev/null +++ b/dotcom-rendering/src/components/AudioPlayer/AudioPlayer.tsx @@ -0,0 +1,355 @@ +import { log } from '@guardian/libs'; +import type { AudioEvent, TAudioEventType } from '@guardian/ophan-tracker-js'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { getOphan } from '../../client/ophan/ophan'; +import { Playback } from './components/Playback'; +import { ProgressBar } from './components/ProgressBar'; +import { CurrentTime, Duration } from './components/time'; +import { Volume } from './components/Volume'; +import { Wrapper } from './components/Wrapper'; + +// possible events for audio in ophan +type AudioEvents = TAudioEventType extends `audio:content:${infer E}` + ? E + : never; + +// possible progress events for audio in ophan +type AudioProgressEvents = Extract< + AudioEvents, + `${number}` +> extends `${infer N extends number}` + ? N + : never; + +type AudioPlayerProps = { + /** The audio source you want to play. */ + src: string; + /** + * Optional, pre-computed duration of the audio source. + * If it's not provided it will be calculated once the audio is loaded. + */ + duration?: number; + /** + * Optionally hide the volume controls if setting the volume is better + * handled elsewhere, e.g on a mobile device. + */ + showVolumeControls?: boolean; + /** media element ID for Ophan */ + mediaId: string; +}; + +/** + * Audio player component. + */ +export const AudioPlayer = ({ + src, + duration: preCalculatedDuration, + showVolumeControls = true, + mediaId, +}: AudioPlayerProps) => { + // ********************* ophan stuff ********************* + + // we'll send listening progress reports to ophan at these percentage points + // through playback (100% is handled by the 'ended' event) + const audioProgressEvents = useRef>( + new Set([25, 50, 75]), + ); + + // wrapper to send audio events to ophan + const reportAudioEvent = useCallback( + (eventName: AudioEvents) => { + const audioEvent: AudioEvent = { + id: mediaId, + eventType: `audio:content:${eventName}`, + }; + + void getOphan('Web').then((ophan) => { + ophan.record({ + audio: audioEvent, + }); + }); + }, + [mediaId], + ); + + // ********************* player ********************* + + // state for displaying feedback to the user + const [isPlaying, setIsPlaying] = useState(false); + const [isMuted, setIsMuted] = useState(false); + const [currentTime, setCurrentTime] = useState(0); + const [duration, setDuration] = useState(preCalculatedDuration); + const [progress, setProgress] = useState(0); + const [isWaiting, setIsWaiting] = useState(false); + const [isScrubbing, setIsScrubbing] = useState(false); + const [buffer, setBuffer] = useState(0); + + const isFirstPlay = useRef(true); + + // ref to the