Spaces:
Sleeping
Sleeping
import { | |
useState, | |
forwardRef, | |
useRef, | |
useImperativeHandle, | |
useEffect, | |
useCallback, | |
} from "react"; | |
const EXAMPLE_URL = | |
"https://huggingface.co/datasets/Xenova/transformers.js-docs/resolve/main/whisper-timestamps-demo.mp4"; | |
const MediaInput = forwardRef( | |
({ onInputChange, onTimeUpdate, ...props }, ref) => { | |
// UI states | |
const [dragging, setDragging] = useState(false); | |
const fileInputRef = useRef(null); | |
// Create a reference to the audio and video elements | |
const audioElement = useRef(null); | |
const videoElement = useRef(null); | |
const currentTimeRef = useRef(0); | |
useImperativeHandle(ref, () => ({ | |
setMediaTime(time) { | |
if (audioElement.current?.src) { | |
audioElement.current.currentTime = time; | |
} else if (videoElement.current?.src) { | |
videoElement.current.currentTime = time; | |
} | |
currentTimeRef.current = time; | |
}, | |
})); | |
const onBufferLoad = (arrayBuffer, type) => { | |
const blob = new Blob([arrayBuffer.slice(0)], { type: type }); | |
const url = URL.createObjectURL(blob); | |
processFile(arrayBuffer); | |
// Create a URL for the Blob | |
if (type.startsWith("audio/")) { | |
// Dispose the previous source | |
videoElement.current.pause(); | |
videoElement.current.removeAttribute("src"); | |
videoElement.current.load(); | |
audioElement.current.src = url; | |
} else if (type.startsWith("video/")) { | |
// Dispose the previous source | |
audioElement.current.pause(); | |
audioElement.current.removeAttribute("src"); | |
audioElement.current.load(); | |
videoElement.current.src = url; | |
} else { | |
alert(`Unsupported file type: ${type}`); | |
} | |
}; | |
const readFile = (file) => { | |
if (!file) return; | |
// file.type | |
const reader = new FileReader(); | |
reader.onload = (e) => { | |
onBufferLoad(e.target.result, file.type); | |
}; | |
reader.readAsArrayBuffer(file); | |
}; | |
const handleInputChange = (event) => { | |
readFile(event.target.files[0]); | |
}; | |
const handleDragOver = (event) => { | |
event.preventDefault(); | |
}; | |
const handleDrop = (event) => { | |
event.preventDefault(); | |
setDragging(false); | |
readFile(event.dataTransfer.files[0]); | |
}; | |
const handleClick = (e) => { | |
if (e.target.tagName === "VIDEO" || e.target.tagName === "AUDIO") { | |
e.preventDefault(); | |
fileInputRef.current.click(); | |
} else if (e.target.tagName === "INPUT") { | |
e.stopPropagation(); | |
} else { | |
fileInputRef.current.click(); | |
e.stopPropagation(); | |
} | |
}; | |
const processFile = async (buffer) => { | |
const audioContext = new (window.AudioContext || | |
window.webkitAudioContext)({ sampleRate: 16_000 }); | |
try { | |
const audioBuffer = await audioContext.decodeAudioData(buffer); | |
let audio; | |
if (audioBuffer.numberOfChannels === 2) { | |
// Merge channels | |
const SCALING_FACTOR = Math.sqrt(2); | |
const left = audioBuffer.getChannelData(0); | |
const right = audioBuffer.getChannelData(1); | |
audio = new Float32Array(left.length); | |
for (let i = 0; i < audioBuffer.length; ++i) { | |
audio[i] = (SCALING_FACTOR * (left[i] + right[i])) / 2; | |
} | |
} else { | |
audio = audioBuffer.getChannelData(0); | |
} | |
onInputChange(audio); | |
} catch (e) { | |
alert(e); | |
} | |
}; | |
const requestRef = useRef(); | |
const updateTime = useCallback(() => { | |
let elem; | |
if (audioElement.current?.src) { | |
elem = audioElement.current; | |
} else if (videoElement.current?.src) { | |
elem = videoElement.current; | |
} | |
if (elem && currentTimeRef.current !== elem.currentTime) { | |
currentTimeRef.current = elem.currentTime; | |
onTimeUpdate(elem.currentTime); | |
} | |
// Request the next frame | |
requestRef.current = requestAnimationFrame(updateTime); | |
}, [onTimeUpdate]); | |
useEffect(() => { | |
// Start the animation | |
requestRef.current = requestAnimationFrame(updateTime); | |
return () => { | |
// Cleanup on component unmount | |
cancelAnimationFrame(requestRef.current); | |
}; | |
}, [updateTime]); | |
return ( | |
<div | |
{...props} | |
onClick={handleClick} | |
onDragOver={handleDragOver} | |
onDrop={handleDrop} | |
onDragEnter={(e) => setDragging(true)} | |
onDragLeave={(e) => setDragging(false)} | |
> | |
<input | |
type="file" | |
accept="audio/*,video/*" | |
onChange={handleInputChange} | |
ref={fileInputRef} | |
className="hidden" | |
/> | |
{ | |
<audio | |
ref={audioElement} | |
controls | |
style={{ display: audioElement.current?.src ? "block" : "none" }} | |
className="w-full max-h-full" | |
/> | |
} | |
{ | |
<video | |
ref={videoElement} | |
controls | |
style={{ display: videoElement.current?.src ? "block" : "none" }} | |
className="w-full max-h-full" | |
/> | |
} | |
{!audioElement.current?.src && !videoElement.current?.src && ( | |
<div | |
className="w-full flex flex-col items-center justify-center border-2 border-dashed border-gray-300 rounded-md h-[250px]" | |
style={{ borderColor: dragging ? "blue" : "lightgray" }} | |
> | |
<span className="text-gray-600 text-center"> | |
<u>Drag & drop</u> or <u>click</u> | |
<br /> | |
to select media | |
</span> | |
<span | |
className="text-gray-500 text-sm hover:text-gray-800 dark:hover:text-gray-300 mt-2" | |
onClick={async (e) => { | |
e.stopPropagation(); | |
const buffer = await fetch(EXAMPLE_URL).then((r) => | |
r.arrayBuffer(), | |
); | |
videoElement.current.src = URL.createObjectURL( | |
new Blob([buffer], { type: "video/mp4" }), | |
); | |
onBufferLoad(buffer, "video/mp4"); | |
}} | |
> | |
(or <u>try an example</u>) | |
</span> | |
</div> | |
)} | |
</div> | |
); | |
}, | |
); | |
MediaInput.displayName = "MediaInput"; | |
export default MediaInput; | |