@theoplayer/react-native-ui
Version:
A React Native UI for @theoplayer/react-native
253 lines (251 loc) • 7.63 kB
JavaScript
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