UNPKG

@theoplayer/react-native-ui

Version:

A React Native UI for @theoplayer/react-native

253 lines (251 loc) 7.63 kB
import React, { useEffect, useState } from 'react'; import { Platform } from 'react-native'; import { Image, View } from 'react-native'; import { isThumbnailTrack } from 'react-native-theoplayer'; import { StaticTimeLabel } from '@theoplayer/react-native-ui'; import { isTileMapThumbnail } from './Thumbnail'; import { URL as URLPolyfill } from './Urlpolyfill'; import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; const SPRITE_REGEX = /^([^#]*)#xywh=(\d+),(\d+),(\d+),(\d+)\s*$/; const TAG = 'ThumbnailView'; export const DEFAULT_THUMBNAIL_VIEW_STYLE = { containerThumbnail: { alignItems: 'center', flexDirection: 'row' }, thumbnail: { overflow: 'hidden', backgroundColor: 'transparent' } }; function getCueIndexAtTime(thumbnailTrack, time) { // Ignore if it's an invalid track or not a thumbnail track. if (!isThumbnailTrack(thumbnailTrack)) { console.warn(TAG, 'Invalid thumbnail track'); return undefined; } // Ignore if the track does not have cues if (thumbnailTrack.cues == null || thumbnailTrack.cues.length == 0) { return undefined; } const cues = thumbnailTrack.cues; let cueIndex = 0; for (const [index, cue] of cues.entries()) { if (cue.startTime <= time) { cueIndex = index; } else if (time >= cue.endTime) { return cueIndex; } } return cueIndex; } function resolveThumbnailUrl(thumbnailTrack, thumbnail) { if (thumbnailTrack && thumbnailTrack.src) { return new URLPolyfill(thumbnail, thumbnailTrack.src).href; } else { return thumbnail; } } function getThumbnailImageForCue(thumbnailTrack, cue) { const thumbnailContent = cue && cue.content; if (!thumbnailContent) { // Cue does not contain any thumbnail info. return null; } const spriteMatch = thumbnailContent.match(SPRITE_REGEX); if (spriteMatch) { // The thumbnail is part of a tile. const [, url, x, y, w, h] = spriteMatch; return { tileX: +x, tileY: +y, tileWidth: +w, tileHeight: +h, url: resolveThumbnailUrl(thumbnailTrack, url) }; } else { // The thumbnail is a separate image. return { url: resolveThumbnailUrl(thumbnailTrack, thumbnailContent) }; } } /** * Calculate the dimensions of the thumbnail tile map based on the W3C Media Fragments URIs for all cues. * * @param thumbnailTrack */ function maxThumbnailSize(thumbnailTrack) { let maxWidth = 0, maxHeight = 0; thumbnailTrack.cues?.forEach(cue => { if (cue && cue.content) { const spriteMatch = cue.content.match(SPRITE_REGEX); if (spriteMatch) { const [,, tileX, tileY, tileWidth, tileHeight] = spriteMatch; maxWidth = Math.max(maxWidth, +tileX + +tileWidth); maxHeight = Math.max(maxHeight, +tileY + +tileHeight); } } }); return { maxTileWidth: maxWidth, maxTileHeight: maxHeight }; } export const ThumbnailView = props => { const [mounted, setMounted] = useState(false); const [imageWidth, setImageWidth] = useState(props.size); const [imageHeight, setImageHeight] = useState(props.size); const [renderWidth, setRenderWidth] = useState(1); const [renderHeight, setRenderHeight] = useState(1); useEffect(() => { setMounted(true); return () => { setMounted(false); }; }, []); const onTileImageLoad = thumbnail => () => { if (!mounted) { return; } const { size } = props; const { tileWidth, tileHeight } = thumbnail; if (tileWidth && tileHeight) { /** * On Android, Fresco can scale the React Native `<Image>` component internally if it is considered to be * 'huge' (larger than 2048px width). There is no way to know the original size without using another * image package. * This work-around calculates the maximum tile size based on all cue W3C Media Fragments URIs. * * {@link https://github.com/facebook/react-native/issues/22145} */ const { maxTileWidth, maxTileHeight } = Platform.OS === 'android' ? maxThumbnailSize(props.thumbnailTrack) : { maxTileWidth: 0, maxTileHeight: 0 }; Image.getSize(thumbnail.url, (width, height) => { setImageWidth(Math.max(width, maxTileWidth)); setImageHeight(Math.max(height, maxTileHeight)); setRenderWidth(size); setRenderHeight(tileHeight * size / tileWidth); }); } }; const onImageLoadError = event => { console.error(TAG, 'Failed to load thumbnail url:', event.nativeEvent.error); }; const onImageLoad = thumbnail => () => { if (!mounted) { return; } const { size } = props; Image.getSize(thumbnail.url, (width, height) => { setImageWidth(width); setImageHeight(height); setRenderWidth(size); setRenderHeight(height * size / width); }); }; const renderThumbnail = (thumbnail, index) => { const { size } = props; const scale = 1.0; if (isTileMapThumbnail(thumbnail)) { const ratio = thumbnail.tileWidth == 0 ? 0 : scale * size / thumbnail.tileWidth; return /*#__PURE__*/_jsx(View, { style: [DEFAULT_THUMBNAIL_VIEW_STYLE.thumbnail, { width: scale * renderWidth, height: scale * renderHeight }], children: /*#__PURE__*/_jsx(Image, { resizeMode: 'cover', style: { position: 'absolute', top: -ratio * thumbnail.tileY, left: -ratio * thumbnail.tileX, width: ratio * imageWidth, height: ratio * imageHeight }, source: { uri: thumbnail.url }, onError: onImageLoadError, onLoad: onTileImageLoad(thumbnail) }) }, index); } else { return /*#__PURE__*/_jsx(View, { style: [DEFAULT_THUMBNAIL_VIEW_STYLE.thumbnail, { width: scale * renderWidth, height: scale * renderHeight }], children: /*#__PURE__*/_jsx(Image, { resizeMode: 'contain', style: { width: scale * size, height: scale * renderHeight }, source: { uri: thumbnail.url }, onError: onImageLoadError, onLoad: onImageLoad(thumbnail) }) }, index); } }; const { time, duration, thumbnailTrack, showTimeLabel, timeLabelStyle } = props; if (!thumbnailTrack || !thumbnailTrack.cues || thumbnailTrack.cues.length === 0) { // No thumbnails to render. return /*#__PURE__*/_jsx(_Fragment, {}); } const nowCueIndex = getCueIndexAtTime(thumbnailTrack, time); if (nowCueIndex === undefined) { // No thumbnail for current time return /*#__PURE__*/_jsx(_Fragment, {}); } const current = getThumbnailImageForCue(thumbnailTrack, thumbnailTrack.cues[nowCueIndex]); if (current === null) { // No thumbnail for current time return /*#__PURE__*/_jsx(_Fragment, {}); } return /*#__PURE__*/_jsxs(View, { style: { flexDirection: 'column' }, children: [showTimeLabel && /*#__PURE__*/_jsx(StaticTimeLabel, { style: [{ marginLeft: 20, height: 20, alignSelf: 'center' }, timeLabelStyle], time: time, duration: duration, showDuration: false }), /*#__PURE__*/_jsx(View, { style: [DEFAULT_THUMBNAIL_VIEW_STYLE.containerThumbnail, { height: renderHeight }], children: renderThumbnail(current, 0) })] }); }; //# sourceMappingURL=ThumbnailView.js.map