From 1950876307589e3a2a2856b24616b604ba442abc Mon Sep 17 00:00:00 2001
From: Vincent <vincent@loki.network>
Date: Wed, 11 Mar 2020 12:40:41 +1100
Subject: [PATCH] Audio playback and pause complete

---
 package.json                                  |   1 -
 .../session/conversation/SessionRecording.tsx | 230 ++++++++++++------
 yarn.lock                                     |  13 -
 3 files changed, 150 insertions(+), 94 deletions(-)

diff --git a/package.json b/package.json
index abc35e504..6a6f99ec7 100644
--- a/package.json
+++ b/package.json
@@ -116,7 +116,6 @@
     "react-autosize-textarea": "^7.0.0",
     "react-contextmenu": "2.11.0",
     "react-dom": "16.8.3",
-    "react-mic": "^12.4.1",
     "react-portal": "^4.2.0",
     "react-qr-svg": "^2.2.1",
     "react-redux": "6.0.1",
diff --git a/ts/components/session/conversation/SessionRecording.tsx b/ts/components/session/conversation/SessionRecording.tsx
index 98ece74e1..0ac9147c8 100644
--- a/ts/components/session/conversation/SessionRecording.tsx
+++ b/ts/components/session/conversation/SessionRecording.tsx
@@ -3,8 +3,6 @@ import moment from 'moment';
 
 import {  SessionIconButton, SessionIconSize, SessionIconType } from '../icon';
 import { SessionButton, SessionButtonType, SessionButtonColor } from '../SessionButton';
-import { Timestamp } from '../../conversation/Timestamp';
-
 
 interface Props {
   onStoppedRecording: any;
@@ -30,6 +28,18 @@ interface State {
     processor: any;
   }
 
+  canvasParams: {
+    width: number;
+    height: number;
+    barRadius: number;
+    barWidth: number;
+    barPadding: number;
+    barColorInit: string;
+    barColorPlay: string;
+    maxBarHeight: number;
+    minBarHeight: number;
+  }
+
   volumeArray?: Array<number>;
 
   startTimestamp: number;
@@ -46,25 +56,32 @@ export class SessionRecording extends React.Component<Props, State> {
   constructor(props: any) {
     super(props);
 
+    // Mouse interaction
     this.handleHoverActions = this.handleHoverActions.bind(this);
     this.handleUnhoverActions = this.handleUnhoverActions.bind(this);
     
+    // Component actions
     this.playAudio = this.playAudio.bind(this);
     this.pauseAudio = this.pauseAudio.bind(this);
     this.stopRecording = this.stopRecording.bind(this);
 
+    // Voice message actions
     this.onSendVoiceMessage = this.onSendVoiceMessage.bind(this);
     this.onDeleteVoiceMessage = this.onDeleteVoiceMessage.bind(this);
 
+    // Stream monitors
     this.timerUpdate = this.timerUpdate.bind(this);
     this.onRecordingStream = this.onRecordingStream.bind(this);
     this.stopRecordingStream = this.stopRecordingStream.bind(this);
 
+    // Refs
     this.visualisationRef = React.createRef();
     this.visualisationCanvas = React.createRef();
     this.playbackCanvas = React.createRef();
 
+    // Listeners
     this.onKeyDown = this.onKeyDown.bind(this);
+    this.updateCanvasDimensions = this.updateCanvasDimensions.bind(this);
 
     const now = Number(moment().format('x')) / 1000;
     const updateTimerInterval = setInterval(this.timerUpdate, 1000);
@@ -84,6 +101,19 @@ export class SessionRecording extends React.Component<Props, State> {
       startTimestamp: now,
       nowTimestamp: now,
       updateTimerInterval,
+
+      // Initial width of 0 until bounds are located
+      canvasParams: {
+        width: 0,
+        height: 35,
+        barRadius: 15,
+        barWidth: 4,
+        barPadding: 3,
+        barColorInit: '#AFAFAF',
+        barColorPlay: '#FFFFFF',
+        maxBarHeight: 30,
+        minBarHeight: 3,
+      },
     };
     
   }
@@ -93,8 +123,14 @@ export class SessionRecording extends React.Component<Props, State> {
     this.initiateRecordingStream();
   }
 
+  public componentDidMount() {
+    window.addEventListener('resize', this.updateCanvasDimensions);
+    this.updateCanvasDimensions();
+  }
+
   public componentWillUnmount(){
     clearInterval(this.state.updateTimerInterval);
+    window.removeEventListener('resize', this.updateCanvasDimensions);
   }
 
   public componentDidUpdate() {
@@ -286,20 +322,51 @@ export class SessionRecording extends React.Component<Props, State> {
         
       };
 
-      
-
       return audioElement;
     }
 
     const audioElement = this.state.audioElement || generateAudioElement();
+    if (!audioElement) return;
     
-    // Start playing recording
-    // FIXME VINCE: Prevent looping of playing
-    audioElement.play();
 
-    // Draw canvas
-    this.onPlaybackStream();
+    // Draw sweeping timeline
+    const drawSweepingTimeline = () => {
+      const { isPaused } = this.state;
+      const {
+        width,
+        height,
+        barColorPlay,
+      } = this.state.canvasParams;
+
+
+      const canvas = this.playbackCanvas.current;
+      if ( !canvas || isPaused ) return;
+
+      // Once audioElement is fully buffered, we get the true duration
+      let audioDuration = this.state.recordDuration
+      if (audioElement.duration !== Infinity) audioDuration = audioElement.duration;
+      const progress = width * (audioElement.currentTime / audioDuration);
 
+      console.log(`[details] Current Time:`, audioElement.currentTime);
+      console.log(`[details] Record Duration:`, audioDuration);
+      console.log(`[details] Audio element duration`, audioElement.duration);
+      
+      const canvasContext = canvas.getContext(`2d`);
+      if (!canvasContext) return;
+
+      canvasContext.beginPath();
+      canvasContext.fillStyle = barColorPlay
+      canvasContext.globalCompositeOperation = 'source-atop';
+      canvasContext.fillRect(0, 0, progress, height);
+
+      // Pause audio when it reaches the end of the blob
+      if (audioElement.duration && audioElement.currentTime === audioElement.duration){
+        this.pauseAudio();
+        return;
+      }
+
+      requestAnimationFrame(drawSweepingTimeline);
+    }
 
     this.setState({
       audioElement,
@@ -308,14 +375,20 @@ export class SessionRecording extends React.Component<Props, State> {
       isPlaying: true,
     });
 
+
+    // If end of audio reached, reset the position of the sweeping timeline
+    if (audioElement.duration && audioElement.currentTime === audioElement.duration){
+      this.initPlaybackView();
+    }
+
+    audioElement.play();
+    requestAnimationFrame(drawSweepingTimeline);
+
   }
   
   private pauseAudio() {
     this.state.audioElement?.pause();
 
-    // STOP ANIMAION FRAME
-    // cancelAnimationFrame(playbackAnimationID);
-
     this.setState({
       isPlaying: false,
       isPaused: true,
@@ -380,7 +453,10 @@ export class SessionRecording extends React.Component<Props, State> {
     // Start recording the stream
     const media = new window.MediaRecorder(stream);
     media.ondataavailable = (mediaBlob: any) => {
-      this.setState({mediaBlob});
+      this.setState({mediaBlob}, () => {
+        // Generate PCM waveform for playback
+        this.initPlaybackView();
+      });
     };
     media.start();
 
@@ -399,34 +475,34 @@ export class SessionRecording extends React.Component<Props, State> {
       const streamParams = {stream, media, input, processor};
       this.setState({streamParams});
 
+      const {
+        width,
+        height,
+        barWidth,
+        barPadding,
+        barColorInit,
+        maxBarHeight,
+        minBarHeight 
+      } = this.state.canvasParams;
+
       // Array of volumes by frequency (not in Hz, arbitrary unit)
       const freqTypedArray = new Uint8Array(analyser.frequencyBinCount);
       analyser.getByteFrequencyData(freqTypedArray);
 
       const freqArray = Array.from(freqTypedArray);
-
-      const VISUALISATION_WIDTH = this.visualisationRef.current?.clientWidth;
       
-      const maxVisualisationHeight = 30;
-      const minVisualisationHeight = 3;
-
       // CANVAS CONTEXT
       const drawRecordingCanvas = () => {
         const canvas = this.visualisationCanvas.current;
-        const CANVAS_HEIGHT = 35;
-        const CANVAS_WIDTH = VISUALISATION_WIDTH || 600;
-
-        const barPadding = 3;
-        const barWidth = 4;
 
-        const numBars = CANVAS_WIDTH / (barPadding + barWidth);
+        const numBars = width / (barPadding + barWidth);
         
         let volumeArray = freqArray.map(n => {
           const maxVal = Math.max(...freqArray);
-          const initialHeight = maxVisualisationHeight * (n / maxVal);
-          const freqBarHeight = initialHeight > minVisualisationHeight
+          const initialHeight = maxBarHeight * (n / maxVal);
+          const freqBarHeight = initialHeight > minBarHeight
             ? initialHeight
-            : minVisualisationHeight;
+            : minBarHeight;
   
           return freqBarHeight;
         });
@@ -440,25 +516,22 @@ export class SessionRecording extends React.Component<Props, State> {
         // Chop off values which exceed the bounds of the container
         volumeArray = volumeArray.slice(0, numBars);
 
-        canvas && (canvas.height = CANVAS_HEIGHT);
-        canvas && (canvas.width = CANVAS_WIDTH);
+        canvas && (canvas.height = height);
+        canvas && (canvas.width = width);
         const canvasContext = canvas && (canvas.getContext(`2d`));
         
         for (var i = 0; i < volumeArray.length; i++) {
           const barHeight = Math.ceil(volumeArray[i]);
           const offset_x = Math.ceil(i * (barWidth + barPadding));
-          const offset_y = Math.ceil((CANVAS_HEIGHT / 2 ) - (barHeight / 2 ));
-          const radius = 15;
+          const offset_y = Math.ceil((height / 2 ) - (barHeight / 2 ));
 
           // FIXME VINCE - Globalise JS references to colors
-          canvasContext && (canvasContext.fillStyle = '#AFAFAF');
+          canvasContext && (canvasContext.fillStyle = barColorInit);
           canvasContext && this.drawRoundedRect(
             canvasContext,
             offset_x,
             offset_y,
-            barWidth,
             barHeight,
-            radius,
           );
         }
       }
@@ -475,7 +548,7 @@ export class SessionRecording extends React.Component<Props, State> {
     return error;
   }
 
-  private compactPCM(array: Float32Array<number>, numGroups: number) {
+  private compactPCM(array: Float32Array, numGroups: number) {
     // Takes an array of arbitary size and compresses it down into a smaller
     // array, by grouping elements into bundles of groupSize and taking their
     // average.
@@ -503,15 +576,23 @@ export class SessionRecording extends React.Component<Props, State> {
     return compacted;
   }
 
-  private async onPlaybackStream() {
-    const VISUALISATION_WIDTH = this.playbackCanvas.current?.clientWidth;
-    const CANVAS_WIDTH = VISUALISATION_WIDTH || 600;
-    const barPadding = 3;
-    const barWidth = 4;
-    const numBars = CANVAS_WIDTH / (barPadding + barWidth);
+  private async initPlaybackView() {
+    const {
+      width,
+      height,
+      barWidth,
+      barPadding,
+      barColorInit,
+      maxBarHeight,
+      minBarHeight 
+    } = this.state.canvasParams;
+
+    const numBars = width / (barPadding + barWidth);
+
+    //FIXME VINCE
+    // update numbars with animation so that changing width of screen 
+    // accomodates
 
-    const startPlayingTimestamp = moment().format('x') / 1000;
-    
     // Then scan through audio file getting average volume per bar
     // to display amplitude over time as a static image
     const blob = this.state.mediaBlob.data;
@@ -519,10 +600,9 @@ export class SessionRecording extends React.Component<Props, State> {
     const arrayBuffer = await new Response(blob).arrayBuffer();
     const audioContext = new window.AudioContext();
     
-    let audioDuration = 0;
     audioContext.decodeAudioData(arrayBuffer, (buffer: AudioBuffer) => {
-      audioDuration = buffer.duration;
-
+      this.setState({recordDuration: buffer.duration});
+      
       // Get audio amplitude with PCM Data in Float32
       // Grab single channel only to save compuation
       const channelData = buffer.getChannelData(0);
@@ -531,76 +611,57 @@ export class SessionRecording extends React.Component<Props, State> {
       const pcmDataArrayNormalised = pcmDataArray.map(v => Math.abs(v));
       
       // Prepare values for drawing to canvas
-      const maxVisualisationHeight = 30;
-      const minVisualisationHeight = 3;
       const maxAmplitude = Math.max(...pcmDataArrayNormalised);
 
       const barSizeArray = pcmDataArrayNormalised.map(amplitude => {
-        let barSize = maxVisualisationHeight * (amplitude / maxAmplitude);
+        let barSize = maxBarHeight * (amplitude / maxAmplitude);
         
         // Prevent values that are too small
-        if (barSize < minVisualisationHeight){
-          barSize = minVisualisationHeight;
+        if (barSize < minBarHeight){
+          barSize = minBarHeight;
         }
 
         return barSize;
       });
 
       // CANVAS CONTEXT
-      let playbackAnimationID;
       const drawPlaybackCanvas = () => {
         const canvas = this.playbackCanvas.current;
-        const CANVAS_HEIGHT = 35;
-
-        canvas.height = CANVAS_HEIGHT;
-        canvas.width = CANVAS_WIDTH;
+        if (!canvas) return;
 
+        canvas.height = height;
+        canvas.width = width;
+          
         const canvasContext = canvas.getContext(`2d`);
+        if (!canvasContext) return;
         
         for (let i = 0; i < barSizeArray.length; i++){
           const barHeight = Math.ceil(barSizeArray[i]);
           const offset_x = Math.ceil(i * (barWidth + barPadding));
-          const offset_y = Math.ceil((CANVAS_HEIGHT / 2 ) - (barHeight / 2 ));
-          const radius = 15;
+          const offset_y = Math.ceil((height / 2 ) - (barHeight / 2 ));
 
           // FIXME VINCE - Globalise JS references to colors
-          canvasContext.fillStyle = '#AFAFAF';
+          canvasContext.fillStyle = barColorInit;
 
           this.drawRoundedRect(
             canvasContext,
             offset_x,
             offset_y,
-            barWidth,
             barHeight,
-            radius,
           );
         }
-
-        // Draw sweeping timeline
-        const now = moment().format('x') / 1000;
-        const progress = CANVAS_WIDTH * ((now - startPlayingTimestamp) / audioDuration);
-        
-        canvasContext.beginPath();
-        canvasContext.fillStyle = '#FFFFFF';
-        canvasContext.globalCompositeOperation = 'source-atop';
-        canvasContext.fillRect(0, 0, progress, CANVAS_HEIGHT);
-        
-        playbackAnimationID = requestAnimationFrame(drawPlaybackCanvas);
       }
 
-      requestAnimationFrame(drawPlaybackCanvas);
+      drawPlaybackCanvas();
 
     });
 
-    // FIXME VINCE SET ASUIDO DURATION TO STATE
-    await this.setState({recordDuration: audioDuration});
-
-    // this.state.isPlaying && requestAnimationFrame(drawPlaybackCanvas);
-      
-
   }
 
-  private drawRoundedRect (ctx: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, r: number) {
+  private drawRoundedRect (ctx: CanvasRenderingContext2D, x: number, y: number, h: number) {
+    let r = this.state.canvasParams.barRadius;
+    const w = this.state.canvasParams.barWidth;
+    
     if (w < 2 * r) r = w / 2;
     if (h < 2 * r) r = h / 2;
     ctx.beginPath();
@@ -613,6 +674,15 @@ export class SessionRecording extends React.Component<Props, State> {
     ctx.fill();
   }
 
+  private updateCanvasDimensions(){
+    const canvas = this.visualisationCanvas.current || this.playbackCanvas.current;
+    const width = canvas?.clientWidth || 0;
+
+    this.setState({
+      canvasParams: {...this.state.canvasParams, width}
+    });
+  }
+
   private onKeyDown(event: any) {
     if (event.key === 'Escape') {
       // FIXME VINCE: Add SessionConfirm
diff --git a/yarn.lock b/yarn.lock
index 003b1fc88..65c437137 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8472,11 +8472,6 @@ react-error-overlay@^4.0.1:
   resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-4.0.1.tgz#417addb0814a90f3a7082eacba7cee588d00da89"
   integrity sha512-xXUbDAZkU08aAkjtUvldqbvI04ogv+a1XdHxvYuHPYKIVk/42BIOD0zSKTHAWV4+gDy3yGm283z2072rA2gdtw==
 
-react-ga@^2.2.0:
-  version "2.7.0"
-  resolved "https://registry.yarnpkg.com/react-ga/-/react-ga-2.7.0.tgz#24328f157f31e8cffbf4de74a3396536679d8d7c"
-  integrity sha512-AjC7UOZMvygrWTc2hKxTDvlMXEtbmA0IgJjmkhgmQQ3RkXrWR11xEagLGFGaNyaPnmg24oaIiaNPnEoftUhfXA==
-
 react-group@^1.0.5:
   version "1.0.6"
   resolved "https://registry.yarnpkg.com/react-group/-/react-group-1.0.6.tgz#8dd7c00c3b35d05ce164021458bb07d580e3001a"
@@ -8506,14 +8501,6 @@ react-lifecycles-compat@^3.0.4:
   resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
   integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
 
-react-mic@^12.4.1:
-  version "12.4.1"
-  resolved "https://registry.yarnpkg.com/react-mic/-/react-mic-12.4.1.tgz#6476a321ccd3babc61ebb12319b0fc0db4ca30c3"
-  integrity sha512-l580F9Mv6NRslSQj+80azx2rr/5OjWISEsjXx2GXBXcLvnT9vGeSYHzFVwDhGBjo/cj25AzXDg+rSp4Bz/qS2w==
-  dependencies:
-    prop-types "^15.5.10"
-    react-ga "^2.2.0"
-
 react-portal@^4.2.0:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/react-portal/-/react-portal-4.2.0.tgz#5400831cdb0ae64dccb8128121cf076089ab1afd"