diff --git a/packages/hls-player/src/controllers/HMSHLSPlayer.ts b/packages/hls-player/src/controllers/HMSHLSPlayer.ts index e32ede13f3..fea74ec1f9 100644 --- a/packages/hls-player/src/controllers/HMSHLSPlayer.ts +++ b/packages/hls-player/src/controllers/HMSHLSPlayer.ts @@ -60,6 +60,7 @@ export class HMSHLSPlayer implements IHMSHLSPlayer, IHMSHLSPlayerEventEmitter { video.autoplay = true; return video; } + /** * @returns get html video element */ @@ -194,6 +195,19 @@ export class HMSHLSPlayer implements IHMSHLSPlayer, IHMSHLSPlayerEventEmitter { this._videoEl.currentTime = seekValue; }; + hasCaptions = () => { + return this._hls.subtitleTracks.length > 0; + }; + + toggleCaption = () => { + // no subtitles, do nothing + if (!this.hasCaptions()) { + return; + } + this._hls.subtitleDisplay = !this._hls.subtitleDisplay; + this.emitEvent(HMSHLSPlayerEvents.CAPTION_ENABLED, this._hls.subtitleDisplay); + }; + private playVideo = async () => { try { if (this._videoEl.paused) { diff --git a/packages/hls-player/src/interfaces/events.ts b/packages/hls-player/src/interfaces/events.ts index 959780cf3c..18aabf0f33 100644 --- a/packages/hls-player/src/interfaces/events.ts +++ b/packages/hls-player/src/interfaces/events.ts @@ -9,6 +9,7 @@ type HMSHLSListenerDataMapping = { [HMSHLSPlayerEvents.TIMED_METADATA_LOADED]: HMSHLSCue; [HMSHLSPlayerEvents.STATS]: HlsPlayerStats; [HMSHLSPlayerEvents.PLAYBACK_STATE]: HMSHLSPlaybackState; + [HMSHLSPlayerEvents.CAPTION_ENABLED]: boolean; [HMSHLSPlayerEvents.ERROR]: HMSHLSException; [HMSHLSPlayerEvents.CURRENT_TIME]: number; diff --git a/packages/hls-player/src/utilies/constants.ts b/packages/hls-player/src/utilies/constants.ts index 7f701b91de..191621bf1e 100644 --- a/packages/hls-player/src/utilies/constants.ts +++ b/packages/hls-player/src/utilies/constants.ts @@ -9,6 +9,7 @@ export enum HMSHLSPlayerEvents { MANIFEST_LOADED = 'manifest-loaded', LAYER_UPDATED = 'layer-updated', + CAPTION_ENABLED = 'caption-enabled', ERROR = 'error', PLAYBACK_STATE = 'playback-state', diff --git a/packages/react-icons/assets/ClosedCaptionIcon.svg b/packages/react-icons/assets/ClosedCaptionIcon.svg index e114166763..a3cd03c537 100644 --- a/packages/react-icons/assets/ClosedCaptionIcon.svg +++ b/packages/react-icons/assets/ClosedCaptionIcon.svg @@ -1,5 +1,5 @@ - - - - + + + + diff --git a/packages/react-icons/assets/OpenCaptionIcon.svg b/packages/react-icons/assets/OpenCaptionIcon.svg new file mode 100644 index 0000000000..12d4fbc585 --- /dev/null +++ b/packages/react-icons/assets/OpenCaptionIcon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/react-icons/src/ClosedCaptionIcon.tsx b/packages/react-icons/src/ClosedCaptionIcon.tsx index 9f31ef7a16..c99ea444ab 100644 --- a/packages/react-icons/src/ClosedCaptionIcon.tsx +++ b/packages/react-icons/src/ClosedCaptionIcon.tsx @@ -1,11 +1,11 @@ import * as React from 'react'; import { SVGProps } from 'react'; const SvgClosedCaptionIcon = (props: SVGProps) => ( - - + ); diff --git a/packages/react-icons/src/OpenCaptionIcon.tsx b/packages/react-icons/src/OpenCaptionIcon.tsx new file mode 100644 index 0000000000..62fef59eb8 --- /dev/null +++ b/packages/react-icons/src/OpenCaptionIcon.tsx @@ -0,0 +1,13 @@ +import * as React from 'react'; +import { SVGProps } from 'react'; +const SvgOpenCaptionIcon = (props: SVGProps) => ( + + + + +); +export default SvgOpenCaptionIcon; diff --git a/packages/react-icons/src/index.ts b/packages/react-icons/src/index.ts index a845226046..0b9ae80e5e 100644 --- a/packages/react-icons/src/index.ts +++ b/packages/react-icons/src/index.ts @@ -167,6 +167,7 @@ export { default as NoEntryIcon } from './NoEntryIcon'; export { default as NotificationsIcon } from './NotificationsIcon'; export { default as OfferIcon } from './OfferIcon'; export { default as OpenBookIcon } from './OpenBookIcon'; +export { default as OpenCaptionIcon } from './OpenCaptionIcon'; export { default as PipIcon } from './PipIcon'; export { default as PadLockOnIcon } from './PadLockOnIcon'; export { default as PaletteIcon } from './PaletteIcon'; diff --git a/packages/roomkit-react/src/Prebuilt/components/HMSVideo/HLSCaptionSelector.tsx b/packages/roomkit-react/src/Prebuilt/components/HMSVideo/HLSCaptionSelector.tsx new file mode 100644 index 0000000000..657daf1b6d --- /dev/null +++ b/packages/roomkit-react/src/Prebuilt/components/HMSVideo/HLSCaptionSelector.tsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { ClosedCaptionIcon, OpenCaptionIcon } from '@100mslive/react-icons'; +import { IconButton } from '../../../'; + +export function HLSCaptionSelector({ isEnabled, onClick }: { isEnabled: boolean; onClick: () => void }) { + return ( + onClick()}> + {isEnabled ? : } + + ); +} diff --git a/packages/roomkit-react/src/Prebuilt/components/HMSVideo/HMSVideo.jsx b/packages/roomkit-react/src/Prebuilt/components/HMSVideo/HMSVideo.jsx index f79bdb3aa4..e71cae5506 100644 --- a/packages/roomkit-react/src/Prebuilt/components/HMSVideo/HMSVideo.jsx +++ b/packages/roomkit-react/src/Prebuilt/components/HMSVideo/HMSVideo.jsx @@ -3,8 +3,44 @@ import { Flex } from '../../../'; export const HMSVideo = forwardRef(({ children, ...props }, videoRef) => { return ( - - + + {children} ); diff --git a/packages/roomkit-react/src/Prebuilt/layouts/HLSView.jsx b/packages/roomkit-react/src/Prebuilt/layouts/HLSView.jsx index 5c36ca0f7c..7dcaafa1eb 100644 --- a/packages/roomkit-react/src/Prebuilt/layouts/HLSView.jsx +++ b/packages/roomkit-react/src/Prebuilt/layouts/HLSView.jsx @@ -16,6 +16,7 @@ import { HlsStatsOverlay } from '../components/HlsStatsOverlay'; import { HMSVideoPlayer } from '../components/HMSVideo'; import { FullScreenButton } from '../components/HMSVideo/FullscreenButton'; import { HLSAutoplayBlockedPrompt } from '../components/HMSVideo/HLSAutoplayBlockedPrompt'; +import { HLSCaptionSelector } from '../components/HMSVideo/HLSCaptionSelector'; import { HLSQualitySelector } from '../components/HMSVideo/HLSQualitySelector'; import { ToastManager } from '../components/Toast/ToastManager'; import { Button } from '../../Button'; @@ -43,6 +44,8 @@ const HLSView = () => { const [availableLayers, setAvailableLayers] = useState([]); const [isVideoLive, setIsVideoLive] = useState(true); const [isUserSelectedAuto, setIsUserSelectedAuto] = useState(true); + const [isCaptionEnabled, setIsCaptionEnabled] = useState(true); + const [hasCaptions, setHasCaptions] = useState(false); const [currentSelectedQuality, setCurrentSelectedQuality] = useState(null); const [isHlsAutoplayBlocked, setIsHlsAutoplayBlocked] = useState(false); const [isPaused, setIsPaused] = useState(false); @@ -61,7 +64,6 @@ const HLSView = () => { onClose: () => toggle(false), }); const [showLoader, setShowLoader] = useState(false); - // FIXME: move this logic to player controller in next release useEffect(() => { /** @@ -103,6 +105,7 @@ const HLSView = () => { let videoEl = videoRef.current; const manifestLoadedHandler = ({ layers }) => { setAvailableLayers(layers); + setHasCaptions(hlsPlayer?.hasCaptions()); }; const layerUpdatedHandler = ({ layer }) => { setCurrentSelectedQuality(layer); @@ -165,6 +168,9 @@ const HLSView = () => { }; const playbackEventHandler = data => setIsPaused(data.state === HLSPlaybackState.paused); + const captionEnabledEventHandler = isCaptionEnabled => { + setIsCaptionEnabled(isCaptionEnabled); + }; const handleAutoplayBlock = data => setIsHlsAutoplayBlocked(!!data); if (videoEl && hlsUrl) { @@ -173,6 +179,7 @@ const HLSView = () => { hlsPlayer.on(HMSHLSPlayerEvents.TIMED_METADATA_LOADED, metadataLoadedHandler); hlsPlayer.on(HMSHLSPlayerEvents.ERROR, handleError); hlsPlayer.on(HMSHLSPlayerEvents.PLAYBACK_STATE, playbackEventHandler); + hlsPlayer.on(HMSHLSPlayerEvents.CAPTION_ENABLED, captionEnabledEventHandler); hlsPlayer.on(HMSHLSPlayerEvents.AUTOPLAY_BLOCKED, handleAutoplayBlock); hlsPlayer.on(HMSHLSPlayerEvents.MANIFEST_LOADED, manifestLoadedHandler); @@ -182,6 +189,8 @@ const HLSView = () => { hlsPlayer.off(HMSHLSPlayerEvents.ERROR, handleError); hlsPlayer.off(HMSHLSPlayerEvents.TIMED_METADATA_LOADED, metadataLoadedHandler); hlsPlayer.off(HMSHLSPlayerEvents.PLAYBACK_STATE, playbackEventHandler); + hlsPlayer.off(HMSHLSPlayerEvents.CAPTION_ENABLED, captionEnabledEventHandler); + hlsPlayer.off(HMSHLSPlayerEvents.AUTOPLAY_BLOCKED, handleAutoplayBlock); hlsPlayer.off(HMSHLSPlayerEvents.MANIFEST_LOADED, manifestLoadedHandler); hlsPlayer.off(HMSHLSPlayerEvents.LAYER_UPDATED, layerUpdatedHandler); @@ -396,6 +405,9 @@ const HLSView = () => { + {hasCaptions && ( + hlsPlayer?.toggleCaption()} isEnabled={isCaptionEnabled} /> + )} {availableLayers.length > 0 ? (