From e4829c8ce1dc8b206ae76cea087f6538410d656f Mon Sep 17 00:00:00 2001
From: Jeremy Kauffman <jeremy@lbry.io>
Date: Wed, 15 Apr 2020 19:21:17 -0400
Subject: [PATCH] broken demonstration

---
 .../viewers/videoViewer/internal/videojs.jsx  | 138 +++++++++
 ui/component/viewers/videoViewer/view.jsx     | 261 +++++-------------
 2 files changed, 200 insertions(+), 199 deletions(-)
 create mode 100644 ui/component/viewers/videoViewer/internal/videojs.jsx

diff --git a/ui/component/viewers/videoViewer/internal/videojs.jsx b/ui/component/viewers/videoViewer/internal/videojs.jsx
new file mode 100644
index 000000000..8fe7e9da9
--- /dev/null
+++ b/ui/component/viewers/videoViewer/internal/videojs.jsx
@@ -0,0 +1,138 @@
+import React, { useContext, useEffect, useRef } from 'react';
+import videojs from 'video.js/dist/alt/video.core.novtt.min.js';
+import 'video.js/dist/alt/video-js-cdn.min.css';
+import eventTracking from 'videojs-event-tracking';
+import { EmbedContext } from '../../../../page/embedWrapper/view';
+import isUserTyping from '../../../../util/detect-typing';
+
+type Props = {
+  source: string,
+  sourceType: string,
+  poster: boolean,
+  autoplay: boolean,
+  onPlayerReady: () => null,
+  isAudio: boolean,
+};
+
+const VIDEO_JS_OPTIONS: VideoJSOptions = {
+  controls: true,
+  autoplay: true,
+  preload: 'auto',
+  playbackRates: [0.25, 0.5, 0.75, 1, 1.1, 1.25, 1.5, 1.75, 2],
+  responsive: true,
+};
+
+type VideoJSOptions = {
+  controls: boolean,
+  autoplay: boolean,
+  preload: string,
+  playbackRates: Array<number>,
+  responsive: boolean,
+  poster?: string,
+  muted?: boolean,
+  poster?: string,
+};
+
+const F11_KEYCODE = 122;
+const SPACE_BAR_KEYCODE = 32;
+const SMALL_F_KEYCODE = 70;
+const SMALL_M_KEYCODE = 77;
+const ARROW_LEFT_KEYCODE = 37;
+const ARROW_RIGHT_KEYCODE = 39;
+
+const FULLSCREEN_KEYCODE = SMALL_F_KEYCODE;
+const MUTE_KEYCODE = SMALL_M_KEYCODE;
+
+const SEEK_FORWARD_KEYCODE = ARROW_RIGHT_KEYCODE;
+const SEEK_BACKWARD_KEYCODE = ARROW_LEFT_KEYCODE;
+
+const SEEK_STEP = 10; // time to seek in seconds
+
+if (!Object.keys(videojs.getPlugins()).includes('eventTracking')) {
+  videojs.registerPlugin('eventTracking', eventTracking);
+}
+
+/*
+properties for this component should be kept to ONLY those that if changed should REQUIRE an entirely new videojs element
+ */
+export default function VideoJs(props: Props) {
+  const { autoplay, source, sourceType, poster, isAudio, onPlayerReady } = props;
+  const videoRef = useRef();
+  const embedded = useContext(EmbedContext);
+  const videoJsOptions = {
+    ...VIDEO_JS_OPTIONS,
+    sources: [
+      {
+        src: source,
+        type: sourceType,
+      },
+    ],
+    poster: poster, // thumb looks bad in app, and if autoplay, flashing poster is annoying
+    plugins: { eventTracking: true },
+  };
+
+  videoJsOptions.autoplay = autoplay;
+  videoJsOptions.muted = autoplay && embedded;
+
+
+  function handleKeyDown(e: KeyboardEvent) {
+    const { current: videoNode } = videoRef;
+
+    if (!videoNode || isUserTyping()) {
+      return;
+    }
+
+    if (e.keyCode === SPACE_BAR_KEYCODE) {
+      videoNode.paused ? videoNode.play() : videoNode.pause();
+    }
+
+    // Fullscreen toggle shortcuts
+    if (e.keyCode === FULLSCREEN_KEYCODE || e.keyCode === F11_KEYCODE) {
+      if (player && !player.isFullscreen()) {
+        player.requestFullscreen();
+      } else {
+        player.exitFullscreen();
+      }
+    }
+
+    // Mute/Unmute Shortcuts
+    if (e.keyCode === MUTE_KEYCODE) {
+      videoNode.muted = !videoNode.muted;
+    }
+
+    // Seeking Shortcuts
+    const duration = videoNode.duration;
+    const currentTime = videoNode.currentTime;
+    if (e.keyCode === SEEK_FORWARD_KEYCODE) {
+      const newDuration = currentTime + SEEK_STEP;
+      videoNode.currentTime = newDuration > duration ? duration : newDuration;
+    }
+    if (e.keyCode === SEEK_BACKWARD_KEYCODE) {
+      const newDuration = currentTime - SEEK_STEP;
+      videoNode.currentTime = newDuration < 0 ? 0 : newDuration;
+    }
+  }
+
+  let player;
+  useEffect(() => {
+    if (videoRef.current) {
+      console.log('videojs effect to instatiate player')
+      const { current: videoNode } = videoRef;
+
+      player = videojs(videoNode, videoJsOptions);
+      onPlayerReady(player);
+
+      window.addEventListener('keydown', handleKeyDown);
+
+      return () => {
+        console.log('videojs effect cleanup to dispose player');
+        window.removeEventListener('keydown', handleKeyDown);
+        player.dispose();
+      };
+    }
+  });
+
+  return <div data-vjs-player>
+    {isAudio ? <audio ref={videoRef} className="video-js" /> : <video ref={videoRef} className="video-js" />}
+  </div>
+}
diff --git a/ui/component/viewers/videoViewer/view.jsx b/ui/component/viewers/videoViewer/view.jsx
index 0830f2e5d..a8d1afc24 100644
--- a/ui/component/viewers/videoViewer/view.jsx
+++ b/ui/component/viewers/videoViewer/view.jsx
@@ -1,9 +1,8 @@
 // @flow
-import React, { useRef, useEffect, useState, useContext } from 'react';
+import React, { useRef, useEffect, useState, useContext, useCallback } from 'react';
 import { stopContextMenu } from 'util/context-menu';
-import videojs from 'video.js/dist/alt/video.core.novtt.min.js';
-import 'video.js/dist/alt/video-js-cdn.min.css';
-import eventTracking from 'videojs-event-tracking';
+import VideoJs from './internal/videojs';
+
 import isUserTyping from 'util/detect-typing';
 import analytics from 'analytics';
 import { EmbedContext } from 'page/embedWrapper/view';
@@ -14,47 +13,11 @@ import usePrevious from 'effects/use-previous';
 import FileViewerEmbeddedEnded from 'lbrytv/component/fileViewerEmbeddedEnded';
 import FileViewerEmbeddedTitle from 'lbrytv/component/fileViewerEmbeddedTitle';
 
-const F11_KEYCODE = 122;
-const SPACE_BAR_KEYCODE = 32;
-const SMALL_F_KEYCODE = 70;
-const SMALL_M_KEYCODE = 77;
-const ARROW_LEFT_KEYCODE = 37;
-const ARROW_RIGHT_KEYCODE = 39;
 
-const FULLSCREEN_KEYCODE = SMALL_F_KEYCODE;
-const MUTE_KEYCODE = SMALL_M_KEYCODE;
 
-const SEEK_FORWARD_KEYCODE = ARROW_RIGHT_KEYCODE;
-const SEEK_BACKWARD_KEYCODE = ARROW_LEFT_KEYCODE;
-
-const SEEK_STEP = 10; // time to seek in seconds
-type VideoJSOptions = {
-  controls: boolean,
-  autoplay: boolean,
-  preload: string,
-  playbackRates: Array<number>,
-  responsive: boolean,
-  poster?: string,
-  muted?: boolean,
-  poseter?: string,
-};
-
-const VIDEO_JS_OPTIONS: VideoJSOptions = {
-  controls: true,
-  autoplay: true,
-  preload: 'auto',
-  playbackRates: [0.25, 0.5, 0.75, 1, 1.1, 1.25, 1.5, 1.75, 2],
-  responsive: true,
-};
-
-if (!Object.keys(videojs.getPlugins()).includes('eventTracking')) {
-  videojs.registerPlugin('eventTracking', eventTracking);
-}
 
 type Props = {
-  volume: number,
   position: number,
-  muted: boolean,
   hasFileInfo: boolean,
   changeVolume: number => void,
   savePosition: (string, number) => void,
@@ -71,14 +34,17 @@ type Props = {
   claimRewards: () => void,
 };
 
+/*
+codesandbox of idealized/clean videojs and react 16+
+https://codesandbox.io/s/71z2lm4ko6
+ */
+
 function VideoViewer(props: Props) {
   const {
     contentType,
     source,
     changeVolume,
     changeMute,
-    volume,
-    muted,
     thumbnail,
     position,
     claim,
@@ -89,185 +55,79 @@ function VideoViewer(props: Props) {
     claimRewards,
   } = props;
   const claimId = claim && claim.claim_id;
-  const videoRef = useRef();
   const isAudio = contentType.includes('audio');
-  const embedded = useContext(EmbedContext);
-
-  if (embedded) {
-    VIDEO_JS_OPTIONS.autoplay = autoplayIfEmbedded;
-    VIDEO_JS_OPTIONS.muted = autoplayIfEmbedded;
-  } else if (autoplaySetting) {
-    VIDEO_JS_OPTIONS.autoplay = autoplaySetting;
-  }
 
   const forcePlayer = FORCE_CONTENT_TYPE_PLAYER.includes(contentType);
-  const [requireRedraw, setRequireRedraw] = useState(false);
   const [isPlaying, setIsPlaying] = useState(false);
   const [showAutoplayCountdown, setShowAutoplayCountdown] = useState(false);
   const [isEndededEmbed, setIsEndededEmbed] = useState(false);
+  const [player, setPlayer] = useState(null);
+
   const previousUri = usePrevious(uri);
+  const embedded = useContext(EmbedContext);
 
-  let player;
+  function doTrackingBuffered(e: Event, data: any) {
+    analytics.videoBufferEvent(claimId, data.currentTime);
+  }
 
-  useEffect(() => {
-    const { current: videoNode } = videoRef;
-    const videoJsOptions = {
-      ...VIDEO_JS_OPTIONS,
-      sources: [
-        {
-          src: source,
-          type: forcePlayer ? 'video/mp4' : contentType,
-        },
-      ],
-      plugins: { eventTracking: true },
-    };
+  function doTrackingFirstPlay(e: Event, data: any) {
+    console.log('doTrackingFirstPlay: ' + data.secondsToLoad);
 
-    // thumb looks bad in app, and if autoplay, flashing poster is annoying
-    if (isAudio || (embedded && !autoplayIfEmbedded)) {
-      videoJsOptions.poster = thumbnail;
+    analytics.videoStartEvent(claimId, data.secondsToLoad);
+
+    doAnalyticsView(uri, data.secondsToLoad).then(() => {
+      claimRewards();
+    });
+  }
+
+  function onEnded() {
+    if (embedded) {
+      setIsEndededEmbed(true);
+    } else if (autoplaySetting) {
+      setShowAutoplayCountdown(true);
     }
+  }
 
-    if (!requireRedraw) {
-      player = videojs(videoNode, videoJsOptions, function() {
-        if (!autoplayIfEmbedded) player.volume(volume);
-        player.muted(autoplayIfEmbedded || muted);
-      });
-    }
+  function onVolumeChange(e: Event) {
+    const isMuted = player.muted();
+    const volume = player.volume();
+    changeVolume(volume);
+    changeMute(isMuted);
+  }
 
-    return () => {
-      if (!player) {
-        return;
-      }
+  function onPlay() {
+    setIsPlaying(true);
+    setShowAutoplayCountdown(false);
+    setIsEndededEmbed(false);
+  }
 
-      // Video.js has a player.dispose() function that is meant to cleanup a previous video
-      // We can't use this because it does some weird stuff to remove the video element from the page
-      // This makes it really hard to use because the ref we keep still thinks it's on the page
-      // requireRedraw just makes it so the video component is removed from the page _by react_
-      // Then it's set to false immediately after so we can re-mount a new player
-      setRequireRedraw(true);
-    };
-  }, [videoRef, source, contentType, setRequireRedraw, requireRedraw]);
+  function onPause() {
+    setIsPlaying(false);
+  }
 
-  useEffect(() => {
-    if (requireRedraw) {
-      setRequireRedraw(false);
-    }
-  }, [requireRedraw]);
-
-  useEffect(() => {
-    function handleKeyDown(e: KeyboardEvent) {
-      const { current: videoNode } = videoRef;
-
-      if (!videoNode || isUserTyping()) {
-        return;
-      }
-
-      if (e.keyCode === SPACE_BAR_KEYCODE) {
-        videoNode.paused ? videoNode.play() : videoNode.pause();
-      }
-
-      // Fullscreen toggle shortcuts
-      if (e.keyCode === FULLSCREEN_KEYCODE || e.keyCode === F11_KEYCODE) {
-        if (!player.isFullscreen()) {
-          player.requestFullscreen();
-        } else {
-          player.exitFullscreen();
-        }
-      }
-
-      // Mute/Unmute Shortcuts
-      if (e.keyCode === MUTE_KEYCODE) {
-        videoNode.muted = !videoNode.muted;
-      }
-
-      // Seeking Shortcuts
-      const duration = videoNode.duration;
-      const currentTime = videoNode.currentTime;
-      if (e.keyCode === SEEK_FORWARD_KEYCODE) {
-        const newDuration = currentTime + SEEK_STEP;
-        videoNode.currentTime = newDuration > duration ? duration : newDuration;
-      }
-      if (e.keyCode === SEEK_BACKWARD_KEYCODE) {
-        const newDuration = currentTime - SEEK_STEP;
-        videoNode.currentTime = newDuration < 0 ? 0 : newDuration;
-      }
-    }
-
-    window.addEventListener('keydown', handleKeyDown);
-    return () => {
-      window.removeEventListener('keydown', handleKeyDown);
-    };
-
-    // include requireRedraw here so the event listener is re-added when we need to manually remove/add the video player
-  }, [videoRef, requireRedraw, player]);
-
-  // player analytics
-  useEffect(() => {
-    function doTrackingBuffered(e: Event, data: any) {
-      analytics.videoBufferEvent(claimId, data.currentTime);
-    }
-
-    function doTrackingFirstPlay(e: Event, data: any) {
-      analytics.videoStartEvent(claimId, data.secondsToLoad);
-
-      doAnalyticsView(uri, data.secondsToLoad).then(() => {
-        claimRewards();
-      });
-    }
-
-    function doEnded() {
-      if (embedded) {
-        setIsEndededEmbed(true);
-      } else if (autoplaySetting) {
-        setShowAutoplayCountdown(true);
-      }
-    }
-
-    function doVolume(e: Event) {
-      const isMuted = player.muted();
-      const volume = player.volume();
-      changeVolume(volume);
-      changeMute(isMuted);
-    }
-
-    function doPlay() {
-      setIsPlaying(true);
-      setShowAutoplayCountdown(false);
-      setIsEndededEmbed(false);
-    }
-
-    if (player) {
+  const onPlayerReady = useCallback(
+    (player) => {
+      console.log('videoViewer.onPlayerReady attach effects');
       player.on('tracking:buffered', doTrackingBuffered);
 
       player.on('tracking:firstplay', doTrackingFirstPlay);
-      // FIXME: above is not firing on subsequent renders (though the effect fires), maybe below check can reset?
-      if (uri && previousUri !== uri) {
-        // do reset?
-      }
 
-      player.on('ended', doEnded);
-      player.on('volumechange', doVolume);
-      player.on('play', doPlay);
-      player.on('pause', () => setIsPlaying(false));
+      player.on('ended', onEnded);
+      player.on('volumechange', onVolumeChange);
+      player.on('play', onPlay);
+      player.on('pause', onPause);
 
       // fixes #3498 (https://github.com/lbryio/lbry-desktop/issues/3498)
       // summary: on firefox the focus would stick to the fullscreen button which caused buggy behavior with spacebar
       // $FlowFixMe
       player.on('fullscreenchange', () => document.activeElement && document.activeElement.blur());
-    }
 
-    return () => {
-      if (player) {
-        player.off();
+      if (position) {
+        player.currentTime(position);
       }
-    };
-  }, [claimId, player, changeVolume, changeMute]); // FIXME: more dependencies?
+    });
 
-  useEffect(() => {
-    if (player && position) {
-      player.currentTime(position);
-    }
-  }, [player, position]);
+  console.log('VideoViewer render');
 
   return (
     <div
@@ -279,11 +139,14 @@ function VideoViewer(props: Props) {
       {showAutoplayCountdown && <AutoplayCountdown uri={uri} />}
       {isEndededEmbed && <FileViewerEmbeddedEnded uri={uri} />}
       {embedded && !isEndededEmbed && <FileViewerEmbeddedTitle uri={uri} />}
-      {!requireRedraw && (
-        <div data-vjs-player>
-          {isAudio ? <audio ref={videoRef} className="video-js" /> : <video ref={videoRef} className="video-js" />}
-        </div>
-      )}
+      <VideoJs
+        source={source}
+        isAudio={isAudio}
+        poster={isAudio || (embedded && !autoplayIfEmbedded) ? thumbnail : null}
+        sourceType={forcePlayer ? 'video/mp4' : contentType}
+        autoplay={embedded ? autoplayIfEmbedded : true}
+        onPlayerReady={() => {}}
+      />
     </div>
   );
 }