]> git.localhorst.tv Git - alttp.git/commitdiff
add APNG player
authorDaniel Karbach <daniel.karbach@localhorst.tv>
Sun, 30 Jul 2023 14:35:01 +0000 (16:35 +0200)
committerDaniel Karbach <daniel.karbach@localhorst.tv>
Sun, 30 Jul 2023 14:35:01 +0000 (16:35 +0200)
package-lock.json
package.json
resources/js/components/common/Icon.js
resources/js/components/common/PngPlayer.js [new file with mode: 0644]
resources/js/i18n/de.js
resources/js/i18n/en.js
resources/sass/common.scss

index 99cc3985917ceaac3744b869d87a1ea6b8f5735f..7bb082eeb408d65f7de1524c8b1dc964c3137a39 100644 (file)
@@ -10,6 +10,7 @@
                 "@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",
index 2429eb1b931ce5454dbcfc361c51caaf6b26c23e..ed394ecb29fcda11e4aaede19ee2247c011badb5 100644 (file)
@@ -76,6 +76,7 @@
         "@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",
index ddd35fee627c139d57be6b6c7722d1fb00151625..022de69f0d2c97e5816a3ec4adfd78a8187c63c6 100644 (file)
@@ -79,8 +79,10 @@ Icon.LOGOUT = makePreset('LogoutIcon', 'sign-out-alt');
 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');
@@ -88,6 +90,9 @@ Icon.RESULT = makePreset('ResultIcon', 'clock');
 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']);
diff --git a/resources/js/components/common/PngPlayer.js b/resources/js/components/common/PngPlayer.js
new file mode 100644 (file)
index 0000000..4bdb41a
--- /dev/null
@@ -0,0 +1,122 @@
+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;
index ae88eaa5ba34d017eaed84bfe9cc3db7594893c4..8f7cba802d4fd369d4bf2a75dd0c7e4220849c1c 100644 (file)
@@ -259,6 +259,9 @@ export default {
                        login: 'Login',
                        logout: 'Logout',
                        new: 'Neu',
+                       nextFrame: 'Nächster Frame',
+                       pause: 'Pause',
+                       play: 'Play',
                        protocol: 'Protokoll',
                        remove: 'Entfernen',
                        retry: 'Neu versuchen',
@@ -266,6 +269,7 @@ export default {
                        search: 'Suche',
                        settings: 'Einstellungen',
                        signUp: 'Anmelden',
+                       stop: 'Stop',
                        unconfirm: 'Zurückziehen',
                },
                crew: {
index dc84f67e90c6421b008691b4871da1581d7d96df..273682fc836dcbf96ff84c7773e36b19098b9e00 100644 (file)
@@ -259,6 +259,9 @@ export default {
                        login: 'Login',
                        logout: 'Logout',
                        new: 'New',
+                       nextFrame: 'Next frame',
+                       pause: 'Pause',
+                       play: 'Play',
                        protocol: 'Protocol',
                        remove: 'Remove',
                        retry: 'Retry',
@@ -266,6 +269,7 @@ export default {
                        search: 'Search',
                        settings: 'Settings',
                        signUp: 'Sign up',
+                       stop: 'Stop',
                        unconfirm: 'Retract',
                },
                crew: {
index 14cbd07be4a3d4178d66b3da3bf0c4a52224b65c..3e88649417c1dcc9b8ced28749acd14768bc205e 100644 (file)
@@ -23,6 +23,31 @@ h1 {
        max-width: none !important;
 }
 
+.png-player {
+       display: flex;
+       flex-direction: column;
+       align-items: center;
+
+       .screen {
+               background: black;
+               display: flex;
+               width: 100%;
+               height: auto;
+
+               canvas {
+                       flex-grow: 1;
+                       -ms-interpolation-mode: nearest-neighbor;
+                       image-rendering: crisp-edges;
+                       image-rendering: pixelated;
+               }
+       }
+
+       .controls {
+               margin-top: 0.5rem;
+               margin-bottom: 0.5rem;
+       }
+}
+
 .quote-alert {
        position: relative;
        &::after {