"@fortawesome/free-brands-svg-icons": "^6.0.0",
"@fortawesome/free-solid-svg-icons": "^6.0.0",
"@fortawesome/react-fontawesome": "^0.1.17",
+ "apng-js": "^1.1.1",
"crc-32": "^1.2.2",
"file-saver": "^2.0.5",
"formik": "^2.2.9",
"node": ">= 8"
}
},
+ "node_modules/apng-js": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/apng-js/-/apng-js-1.1.1.tgz",
+ "integrity": "sha512-UWaloDssWCE8Bj0wipyNxEXPnMadYS0VAjghCLas5nKGqfiBMNdQJhg8Fawq2+jZ50IOM1feKwjiqPAC/bvKgg=="
+ },
"node_modules/arg": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.1.tgz",
"picomatch": "^2.0.4"
}
},
+ "apng-js": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/apng-js/-/apng-js-1.1.1.tgz",
+ "integrity": "sha512-UWaloDssWCE8Bj0wipyNxEXPnMadYS0VAjghCLas5nKGqfiBMNdQJhg8Fawq2+jZ50IOM1feKwjiqPAC/bvKgg=="
+ },
"arg": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.1.tgz",
Icon.MICROPHONE = makePreset('MicrophoneIcon', 'microphone');
Icon.MONITOR = makePreset('MonitorIcon', 'tv');
Icon.MOUSE = makePreset('MouseIcon', 'arrow-pointer');
+Icon.PAUSE = makePreset('PauseIcon', 'pause');
Icon.PENDING = makePreset('PendingIcon', 'clock');
Icon.PIN = makePreset('PinIcon', 'location-pin');
+Icon.PLAY = makePreset('PlayIcon', 'play');
Icon.PROTOCOL = makePreset('ProtocolIcon', 'file-alt');
Icon.REJECT = makePreset('RejectIcon', 'square-xmark');
Icon.REMOVE = makePreset('RemoveIcon', 'square-xmark');
Icon.SECOND_PLACE = makePreset('SecondPlaceIcon', 'medal');
Icon.SETTINGS = makePreset('SettingsIcon', 'cog');
Icon.SLASH = makePreset('SlashIcon', 'slash');
+Icon.STEP_BACKWARD = makePreset('StepBackwardIcon', 'backward-step');
+Icon.STEP_FORWARD = makePreset('StepForwardIcon', 'forward-step');
+Icon.STOP = makePreset('StopIcon', 'stop');
Icon.STREAM = makePreset('StreamIcon', ['fab', 'twitch']);
Icon.THIRD_PLACE = makePreset('ThirdPlaceIcon', 'award');
Icon.TWITCH = makePreset('TwitchIcon', ['fab', 'twitch']);
--- /dev/null
+import parseApng from 'apng-js';
+import axios from 'axios';
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Button } from 'react-bootstrap';
+import { useTranslation } from 'react-i18next';
+
+import Icon from './Icon';
+
+const createPlayer = async (apng, canvas) => {
+ const context = canvas.getContext('2d', { willReadFrequently: true });
+ const player = await apng.getPlayer(context);
+ player.stop();
+ return player;
+};
+
+const PngPlayer = ({ src }) => {
+ const canvas = React.useRef();
+ const { t } = useTranslation();
+
+ const [apng, setApng] = React.useState(null);
+ const [error, setError] = React.useState(null);
+ const [loading, setLoading] = React.useState(true);
+ const [player, setPlayer] = React.useState(null);
+
+ React.useEffect(() => {
+ setError(null);
+ setLoading(true);
+ const ctrl = new AbortController();
+ const fetchPng = async () => {
+ try {
+ const response = await axios.get(src, {
+ responseType: 'arraybuffer',
+ signal: ctrl.signal,
+ });
+ const png = parseApng(response.data);
+ await png.createImages();
+ setApng(png);
+ setLoading(false);
+ } catch (e) {
+ if (!axios.isCancel(e)) {
+ setError(e);
+ console.log(e);
+ }
+ }
+ };
+ fetchPng();
+ return () => {
+ ctrl.abort();
+ };
+ }, [src]);
+
+ React.useEffect(async () => {
+ if (loading || !canvas.current) return;
+ setPlayer(await createPlayer(apng, canvas.current));
+ }, [apng, canvas.current, loading]);
+
+ const play = React.useCallback(() => {
+ if (player) player.play();
+ }, [player]);
+
+ const pause = React.useCallback(() => {
+ if (player) player.pause();
+ }, [player]);
+
+ const stop = React.useCallback(() => {
+ if (player) player.stop();
+ }, [player]);
+
+ const nextFrame = React.useCallback(() => {
+ if (player) player.renderNextFrame();
+ }, [player]);
+
+ if (error) {
+ return <div>Error</div>;
+ }
+ if (loading) {
+ return <div>Loading</div>;
+ }
+
+ return <div className="png-player">
+ <div className="screen">
+ <canvas ref={canvas} width={apng.width} height={apng.height} />
+ </div>
+ <div className="button-bar controls">
+ <Button
+ onClick={stop}
+ title={t('button.stop')}
+ variant="outline-secondary"
+ >
+ <Icon.STOP title="" />
+ </Button>
+ <Button
+ onClick={play}
+ title={t('button.play')}
+ variant="outline-secondary"
+ >
+ <Icon.PLAY title="" />
+ </Button>
+ <Button
+ onClick={pause}
+ title={t('button.pause')}
+ variant="outline-secondary"
+ >
+ <Icon.PAUSE title="" />
+ </Button>
+ <Button
+ onClick={nextFrame}
+ title={t('button.nextFrame')}
+ variant="outline-secondary"
+ >
+ <Icon.STEP_FORWARD title="" />
+ </Button>
+ </div>
+ </div>;
+};
+
+PngPlayer.propTypes = {
+ src: PropTypes.string,
+};
+
+export default PngPlayer;