Skip to content

Instantly share code, notes, and snippets.

@futurepaul
Created March 2, 2020 15:40
Show Gist options
  • Save futurepaul/e4f099c8689815f394e8a2299ce78f26 to your computer and use it in GitHub Desktop.
Save futurepaul/e4f099c8689815f394e8a2299ce78f26 to your computer and use it in GitHub Desktop.
Code on tape (meta)
import React, { useState, useRef } from "react";
function decimalToTime(seconds, duration) {
let fmtTime = new Date(1000 * seconds).toISOString().substr(11, 8);
if (duration >= 3600) {
return fmtTime;
} else {
//remove the hours
fmtTime = fmtTime.substring(3);
if (duration >= 600) {
return fmtTime;
} else {
//remove the tens minute
return fmtTime.substring(1);
}
}
}
const AudioPlayer = React.forwardRef(
(
{ audioSrcUrl, onClickPlay, onMouseDown, onMouseUp, playing, setPlaying },
audioPlayerEl
) => {
const [muted, setMuted] = useState(false);
const [timeString, setTimeString] = useState("0:00 / 0:00");
const [progress, setProgress] = useState(0);
// const audioPlayerEl = ref;
const play = () => {
onClickPlay();
if (!playing) {
try {
audioPlayerEl.current.play();
setPlaying(true);
} catch (e) {
console.error(e);
}
} else {
audioPlayerEl.current.pause();
setPlaying(false);
}
};
const mute = () => {
audioPlayerEl.current.muted = !muted;
setMuted(!muted);
};
const timeUpdate = e => {
let audio = audioPlayerEl.current;
setProgress((100 * audio.currentTime) / audio.duration);
// Report back to our parent
// onTimeUpdate(audio.currentTime);
// console.log("current time:" + audio.currentTime);
let time = `${decimalToTime(
audio.currentTime,
audio.duration
)} / ${decimalToTime(audio.duration, audio.duration)}`;
setTimeString(time);
if (audio.ended) setPlaying(false);
};
const playHeadInput = evt => {
let audio = audioPlayerEl.current;
const scrubPercent = evt.target.value;
let scrubDestination = (scrubPercent / 100) * audio.duration;
audio.currentTime = scrubDestination;
};
return (
<>
<div className="player">
<audio
ref={audioPlayerEl}
src={audioSrcUrl}
onTimeUpdate={timeUpdate}
></audio>
<button className={`play ${playing && "playing"}`} onClick={play}>
{playing ? "Pause" : "Play"}
</button>
<div className="time">{timeString}</div>
<input
className="playHead"
value={progress}
type="range"
min="0"
max="100"
step=".1"
onInput={playHeadInput}
onChange={() => {}}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
/>
<button
className={`mute ${muted && "muted"}`}
onClick={mute}
></button>
</div>
<style jsx>{`
.player {
width: 100%;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
}
input[type="range"] {
height: 1rem;
-webkit-appearance: none;
flex-grow: 1;
padding-top: 1rem;
padding-bottom: 1rem;
padding-right: 3px;
border: 1px solid white;
}
input[type="range"]:focus {
outline: none;
}
input[type="range"]:-moz-focusring {
outline: 1px solid white;
outline-offset: -1px;
}
input[type="range"]::-webkit-slider-runnable-track {
height: calc(1rem + 2px);
box-shadow: 3px 3px grey;
background-image: url(/svg/square.svg);
background-size: var(--load-percentage) 100px;
background-repeat: no-repeat;
border-radius: 0px;
border: 1px solid #000000;
}
input[type="range"]::-moz-range-track {
height: calc(1rem + 2px);
box-shadow: 3px 3px grey;
background-image: url(/svg/square.svg);
background-size: var(--load-percentage) 100px;
background-repeat: no-repeat;
border-radius: 0px;
border: 1px solid #000000;
}
input[type="range"]::-webkit-slider-thumb {
height: 1rem;
width: 1rem;
border-radius: 0px;
background: black;
-webkit-appearance: none;
}
input[type="range"]::-moz-range-thumb {
height: 1rem;
width: 1rem;
border-radius: 0px;
background: black;
-webkit-appearance: none;
}
.player button {
width: 1rem;
height: 1rem;
border: none;
outline: none;
background-repeat: no-repeat;
background-position: center;
padding: 1rem;
}
.time {
width: 6rem;
text-align: center;
flex: none;
}
button.play {
padding-left: 0rem;
background-color: black;
-webkit-mask: url(/svg/sharp-play_arrow-24px.svg) no-repeat 50% 50%;
mask: url(/svg/sharp-play_arrow-24px.svg) no-repeat 50% 50%;
}
button.play.playing {
-webkit-mask: url(/svg/sharp-pause-24px.svg) no-repeat 50% 50%;
mask: url(/svg/sharp-pause-24px.svg) no-repeat 50% 50%;
}
button.mute {
-webkit-mask: url(/svg/sharp-volume_up-24px.svg) no-repeat 50% 50%;
background-image: url(/svg/sharp-volume_up-24px.svg);
}
button.mute.muted {
-webkit-mask: url(/svg/sharp-volume_off-24px.svg) no-repeat 50% 50%;
background-image: url(/svg/sharp-volume_off-24px.svg);
}
`}</style>
</>
);
}
);
export default AudioPlayer;
import { useState, useRef, useEffect } from "react";
import Editor from "../components/Editor/Editor";
import AudioPlayer from "../components/AudioPlayer";
import Tabs from "../components/Tabs";
import useInterval from "../hooks/useInterval";
function findClosestEvent(scrubTime, events) {
const length = events.length;
const totalTime = events[length - 1].time;
let guess = Math.floor((scrubTime / totalTime) * length);
// console.log(`length: ${length}, totalTime: ${totalTime}, guess: ${guess}`);
while (guess > 1 && guess < length - 1) {
if (
scrubTime <= events[guess + 1].time &&
scrubTime >= events[guess].time
) {
break;
} else if (scrubTime > events[guess + 1].time) {
guess += 1;
} else if (scrubTime < events[guess].time) {
guess -= 1;
} else {
break;
}
}
return Math.min(guess, length - 1);
}
function calculateDelay(startTime, stamp) {
let currentTime = performance.now();
let delay = Math.floor(stamp - (currentTime - startTime));
if (delay < 1) {
delay = 1;
}
console.log(`delay: ${delay}, ct: ${currentTime}, st: ${startTime}`);
return delay;
}
const Play = ({ gistID, files, eventLog, audio }) => {
// Meta playback state
const [playbackStartTime, setPlaybackStartTime] = useState(null);
const [playing, setPlaying] = useState(false);
const [following, setFollowing] = useState(true);
const [interval, setNextInterval] = useState(0);
const [isScrubbing, setIsScrubbing] = useState(false);
const audioRef = useRef(null);
// Current playback state
const [cursor, setCursor] = useState({ lineNumber: 1, column: 1 });
const [activeTab, setActiveTab] = useState(0);
const [index, setIndex] = useState(0);
// This is called after we release the slider of the aduio player.
const onPostScrub = () => {
audioRef.current.pause();
setNextInterval(null);
setPlaying(false);
let t = audioRef.current.currentTime;
let ms = Math.round(t * 1000);
let newIndex = findClosestEvent(ms, eventLog);
let event = eventLog[newIndex];
setIndex(newIndex);
setActiveTab(event.tab);
setCursor(event.cursor);
};
const continuePlayback = () => {
// let t = audioRef.current.currentTime;
// let ms = Math.round(t * 1000);
// let newIndex = findClosestEvent(ms, eventLog);
if (index < eventLog.length - 1) {
let newPlaybackStartTime = performance.now() - eventLog[index].time;
let delay = calculateDelay(newPlaybackStartTime, eventLog[index].time);
setPlaybackStartTime(newPlaybackStartTime);
setNextInterval(delay);
} else {
setNextInterval(null);
setPlaying(false);
}
};
const startPlaying = () => {
if (!playing) {
audioRef.current
.play()
.then(() => {
setPlaying(true);
continuePlayback();
})
.catch(e => {
console.error(e);
});
} else {
setNextInterval(null);
audioRef.current.pause();
setPlaying(false);
}
};
const onKeyPress = e => {
if (e.charCode === 32) {
startPlaying();
}
};
useInterval(
() => {
if (index + 1 < eventLog.length) {
let currentEvent = eventLog[index];
if (following) {
setActiveTab(currentEvent.tab);
setCursor(currentEvent.cursor);
}
let delay = calculateDelay(playbackStartTime, eventLog[index + 1].time);
// let errorCalc =
// playbackStartTime + eventLog[index].time - performance.now();
// console.log(`error amount: ${errorCalc}`);
setNextInterval(delay);
setIndex(index + 1);
} else {
let currentEvent = eventLog[index];
if (following) {
setActiveTab(currentEvent.tab);
setCursor(currentEvent.cursor);
}
setPlaying(false);
}
},
playing ? interval : null
);
const requestActiveTab = id => {
setActiveTab(id);
};
//TODO: handle missing audio
return (
<div onKeyPress={onKeyPress}>
<AudioPlayer
onClickPlay={startPlaying}
audioSrcUrl={audio}
onMouseDown={() => setIsScrubbing(true)}
onMouseUp={onPostScrub}
ref={audioRef}
playing={playing}
setPlaying={setPlaying}
/>
<Tabs
activeTab={activeTab}
requestActiveTab={requestActiveTab}
files={files}
/>
<Editor
gist={files}
tabID={activeTab}
cursor={cursor}
onCursorChange={e => console.log("on cursor change:" + e)}
/>
<style jsx>{`
.nav {
display: flex;
justify-content: flex-start;
align-items: flex-end;
border-bottom: 2px solid black;
padding-left: 1rem;
}
`}</style>
</div>
);
};
export default Play;
import { useState, useContext, useEffect } from "react";
import Editor from "../../components/Editor/Editor";
import RecordControls from "../../components/RecordControls";
import Tabs from "../../components/Tabs";
import WarningBanner from "../../components/WarningBanner";
import EditorContext from "../../context/editor/editorContext";
import Router from "next/router";
import fetch from "isomorphic-unfetch";
import useInterval from "../../hooks/useInterval";
import Layout from "../../components/Layout";
const defaultCursor = { lineNumber: 1, column: 1 };
const Record = ({ gistID, files }) => {
// App state
const [cursor, setCursor] = useState(defaultCursor);
const [activeTab, setActiveTab] = useState(0);
const [perTabCursor, setPerTabCursor] = useState([]);
// Event recording logic
const [eventLog, setEventLog] = useState(null);
const [recordingStartTime, setRecordingStartTime] = useState(null);
const [isRecording, setIsRecording] = useState(null);
const [timeSoFar, setTimeSoFar] = useState(null);
// Editor context for forwarding state to the playback preview
const editorContext = useContext(EditorContext);
const { setGists, setGistID, saveEventLog, recordingError } = editorContext;
const setTabAndCursor = (tab, cursor) => {
if (isRecording) {
let event = {
time: Math.floor(performance.now() - recordingStartTime),
cursor,
tab
};
setEventLog(eventLog.concat(event));
}
// console.log(
// `tab: ${tab}, cursor: { line: ${cursor.lineNumber}, column: ${cursor.column}}`
// );
let tempPerTabCursor = perTabCursor;
tempPerTabCursor[tab] = cursor;
setPerTabCursor(tempPerTabCursor);
setActiveTab(tab);
setCursor(cursor);
};
const onClickRecord = (shouldStart, startTime) => {
if (shouldStart && startTime) {
setIsRecording(true);
setEventLog([{ time: 0, cursor: cursor, tab: activeTab }]);
console.log("Starting recording");
setRecordingStartTime(startTime);
} else {
setIsRecording(false);
console.log("Stopping recording");
console.log(eventLog);
}
};
const onCursorChange = e => {
let newCursor = { lineNumber: e.lineNumber, column: e.column };
setTabAndCursor(activeTab, newCursor);
};
const requestActiveTab = newTabID => {
let cursor = perTabCursor[newTabID]
? perTabCursor[newTabID]
: defaultCursor;
console.log(perTabCursor);
setTabAndCursor(newTabID, cursor);
};
const gotoPlaybackPreview = () => {
setGists(files);
setGistID(gistID);
saveEventLog(eventLog);
Router.push("/play");
};
useInterval(
() => {
setTabAndCursor(activeTab, cursor);
setTimeSoFar(Math.floor(performance.now() - recordingStartTime));
console.log(
`Set tab: ${activeTab} and cursor: l: ${cursor.lineNumber}, c: ${cursor.column} using interval`
);
},
isRecording ? 1000 : null
);
const success = (
<WarningBanner>
Recorded!{" "}
<button className="continue" onClick={gotoPlaybackPreview}>
{" "}
Go to playback preview{" "}
</button>{" "}
<button className="danger" onClick={() => location.reload()}>
Clear recording and start over
</button>
</WarningBanner>
);
const microphone_fail = (
<WarningBanner>
<strong>Error:</strong> Something went wrong. Did you say yes to the
microphone? <button onClick={() => location.reload()}>Start over</button>
</WarningBanner>
);
const browser_fail = (
<WarningBanner>
<strong>Error:</strong> Sorry, your browser isn't supported!
</WarningBanner>
);
return (
<Layout title="Record">
{recordingError === "microphone" && microphone_fail}
{recordingError === "browser" && browser_fail}
{!isRecording &&
eventLog &&
eventLog.length > 1 &&
recordingError === null
? success
: recordingError === null && (
<RecordControls
onClickRecord={onClickRecord}
cursor={cursor}
timeSoFar={timeSoFar}
/>
)}
<Tabs
activeTab={activeTab}
requestActiveTab={requestActiveTab}
files={files}
/>
<Editor
gist={files}
tabID={activeTab}
cursor={cursor}
onCursorChange={onCursorChange}
/>
</Layout>
);
};
const client_id = process.env.GITHUB_CLIENT_ID;
const client_secret = process.env.GITHUB_CLIENT_SECRET;
Record.getInitialProps = async ctx => {
let query = ctx.query.id;
try {
let url = `https://api.github.com/gists/${query}?client_id=${client_id}&client_secret=${client_secret}`;
const res = await fetch(url);
let json = await res.json();
let gistFiles = json.files;
let files = Object.keys(gistFiles).map(key => gistFiles[key]);
return { gistID: query, files: files };
} catch (error) {
console.error(error);
return {
gistID: null,
gists: null
};
}
};
export default Record;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment