To better explain what I mean, I am trying to create an application (HTML/JavaScript) that allows users to record their screens (both visuals and system sounds), and get a video (an mp4 file) from it. I have actually long since succeeded in that part. However, I also want to add functionality where the user could select a start time and end time of the video, and create a trimmed video containing just that selected portion of the original video. Here is some code that I have:
<!DOCTYPE html>
<html>
<head>
<style>
.videoBox {
border-style: solid;
border-color: black;
width: 310px;
height: 310px;
margin-bottom: 20px;
}
.videoControls {
margin-left: 50px;
margin-top: 50px;
}
.playOrPauseButton {
margin-bottom: 10px;
}
.endButton {
margin-left: 10px;
}
.savingInfo {
margin-top: 10px;
}
</style>
</head>
<body>
<input type="hidden" id="recording" value="0" />
<div class="videoBox">
<div class="videoControls">
<div class="playOrPauseButton">
<button type="button" id="record" onclick="playOrPause()">Start Recording</button>
<div class="recordingTime" id="recordingTime"></div>
</div>
<div class="endButton">
<a id="downloadLink"><button type="button" id="endRecording" onclick="stopRecording()">Save
Video</button></a>
</div>
<div class="savingInfo">
Your video will download, and it will also appear below when you save your video.
</div>
</div>
</div>
<div id="videoItself" class="videoItself"></div>
<p id="chunky"></p>
<button type="button" onclick="getCurrentTime()">Get current time</button>
<button type="button" onclick="getChunkZeroHeaderInfo()">Chunk 0 header</button>
<button type="button" onclick="alertChunkZero()">Chunk 0 full</button>
<button type="button" onclick="getChunkNumber()">Chunk (number) full</button>
<input type="number" id="chunkNumber"/>
<p id="time"></p>
<script>
numberOfClicks = 1;
let chunks = [];
let mediaStream = null;
let recorder = null;
let seconds = 0;
let videoNumber = 1;
let filename = "";
let videoLink = document.getElementById("downloadLink");
let videoURL = "";
let resumePressed = false;
let newChunks = [];
let inc = 0;
let chunkZeroHeaderInfo = "";
let chunkZeroFull = "";
let chunkBuffers = [];
async function setupMediaStream() {
mediaStream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: true });
recorder = new MediaRecorder(mediaStream);
recorder.addEventListener("dataavailable", function (e) {
if(!resumePressed) {
chunks.push(e.data);
}
resumePressed = false;
document.getElementById("chunky").innerHTML = chunks.length;
});
recorder.onstop = function () {
document.getElementById("videoItself").innerHTML = "";
if (filename.trim().length == 0) {
filename = "video " + videoNumber;
}
videoNumber++;
let mostRecentChunk = [];
//mostRecentChunk.push(chunks[chunks.length - 1]);
mostRecentChunk.push(...chunks);
let blob = new Blob(mostRecentChunk, { 'type': 'video/mp4' });
videoURL = window.URL.createObjectURL(blob);
videoLink.setAttribute("href", videoURL);
videoLink.setAttribute("download", filename);
document.getElementById("endRecording").click();
videoLink.removeAttribute("download");
videoLink.removeAttribute("href");
document.getElementById("recordingTime").innerHTML = "";
document.getElementById("recording").value = 0;
document.getElementById("record").innerHTML = "Start Recording";
let video = document.createElement("video");
video.src = videoURL;
video.controls = true;
video.style.width = "400px";
video.style.height = "400px";
video.setAttribute("id", "theVideo");
document.getElementById("videoItself").appendChild(video);
let seekBox = document.createElement("input");
seekBox.setAttribute("type", "number");
seekBox.setAttribute("id", "seekBox");
let seekButton = document.createElement("button");
seekButton.setAttribute("id", "seekButton");
seekButton.innerHTML = "Seek Time (seconds)";
seekButton.setAttribute("onclick", "seekTime()");
let videoLength = document.createElement("p");
videoLength.setAttribute("id", "videoLength");
document.getElementById("videoItself").appendChild(seekBox);
document.getElementById("videoItself").appendChild(seekButton);
videoLength.innerHTML = "The length of the video is " + seconds + " seconds";
document.getElementById("videoItself").appendChild(videoLength);
seconds = 0;
//alert(chunks.length);
let trimPrompt = document.createElement("p");
trimPrompt.setAttribute("id", "trimPrompt");
trimPrompt.innerHTML = "Do you want to trim the video at all?:";
let startTimeBox = document.createElement("input");
startTimeBox.setAttribute("type", "number");
startTimeBox.setAttribute("id", "startTimeBox");
startTimeBox.setAttribute("placeholder", "start time (seconds)");
let endTimeBox = document.createElement("input");
endTimeBox.setAttribute("type", "number");
endTimeBox.setAttribute("id", "endTimeBox");
endTimeBox.setAttribute("placeholder", "end time (seconds)");
let trimButton = document.createElement("button");
trimButton.setAttribute("id", "trimButton");
trimButton.innerHTML = "Trim";
trimButton.setAttribute("onclick", "trueTrim()");
document.body.appendChild(trimPrompt);
document.body.appendChild(startTimeBox);
document.body.appendChild(endTimeBox);
document.body.appendChild(trimButton);
setChunkZeroHeaderInfo();
setUpBuffers();
};
}
setupMediaStream();
function seekTime() {
let seekBox = document.getElementById("seekBox");
let video = document.getElementById("theVideo");
video.currentTime = seekBox.value;
}
async function getChunkNumberText(number) {
return await chunks[number].text();
}
function getChunkNumber() {
let number = document.getElementById("chunkNumber").value;
getChunkNumberText(number).then(function(value){
alert(value);
});
}
function overwriteChunkZero(startTime, endTime) {
newChunks[startTime] = new Blob([chunks[0].slice(0, chunkZeroHeaderInfo.length), chunks[startTime]]);
newChunks = newChunks.slice(startTime, endTime);
}
async function setUpBuffers() {
for(let i = 0; i < chunks.length; i++) {
let buffer = await chunks[i].arrayBuffer();
let uint8 = new Uint8Array(buffer);
chunkBuffers.push(uint8);
}
}
async function getChunkZeroText() {
return await chunks[0].text();
}
function setChunkZeroHeaderInfo() {
let keyString = "V_MPEG4/ISO/AVC";
getChunkZeroText().then(function(value) {
chunkZeroFull = value;
chunkZeroHeaderInfo = value.substring(0, value.indexOf("V_MPEG4/ISO/AVC") + keyString.length);
});
}
function alertChunkZero() {
alert(chunkZeroHeaderInfo.length);
alert(chunkZeroFull);
alert(chunkZeroFull.length);
}
function getChunkZero() {
return chunkZeroFull;
}
function getChunkZeroHeaderInfo() {
alert(chunkZeroHeaderInfo);
}
function getCurrentTime() {
let video = document.getElementById("theVideo");
alert(video.currentTime);
}
function trueTrim() {
let startTime = document.getElementById("startTimeBox").value;
let endTime = document.getElementById("endTimeBox").value;
let timeSlice = chunks.slice(startTime, endTime);
if(document.getElementById("trimmedVideo") != undefined) {
document.body.removeChild(document.getElementById("trimmedVideo"));
document.body.removeChild(document.getElementById("trimmedLink"));
}
if(startTime != 0) {
newChunks = [];
let normalZero = Array.from(chunkBuffers[0]);
let normalZeroHeader = normalZero.slice(0, chunkZeroHeaderInfo.length);
let newZero = [];
newZero.push(...normalZeroHeader);
let normalStart = Array.from(chunkBuffers[startTime]);
for(let i = 0; i < normalStart.length; i++) {
newZero.push(normalStart[i]);
}
let uint8 = new Uint8Array(newZero);
let newBlob = new Blob([uint8]);
newChunks.push(newBlob);
timeSlice = timeSlice.slice(1, timeSlice.length);
newChunks.push(...timeSlice);
trimVideo();
} else {
newChunks = [];
newChunks.push(...timeSlice);
trimVideo();
}
}
function trimVideo() {
let trimmedBlob = new Blob(newChunks, { 'type': 'video/mp4' });
let trimmedURL = window.URL.createObjectURL(trimmedBlob);
let trimmedVideo = document.createElement("video");
trimmedVideo.src = trimmedURL;
trimmedVideo.controls = true;
trimmedVideo.style.width = "400px";
trimmedVideo.style.height = "400px";
trimmedVideo.setAttribute("id", "trimmedVideo");
let trimmedLink = document.createElement("a");
trimmedLink.setAttribute("id", "trimmedLink");
trimmedLink.setAttribute("href", trimmedURL);
trimmedLink.setAttribute("download", filename + " trimmed");
trimmedLink.innerHTML = "Click here to download the trimmed video";
document.body.appendChild(trimmedVideo);
document.body.appendChild(trimmedLink);
}
function playOrPause() {
const recording = parseInt(document.getElementById("recording").value);
recording == 0 ? record() : pause();
}
function record() {
if (recorder != null) {
try {
if(recorder.state != "paused") {
chunks = [];
}
recorder.start(1000);
} catch (err) {
recorder.resume();
resumePressed = true;
}
myInterval = setInterval(countTime, 1000);
document.getElementById("recording").value = 1;
document.getElementById("record").innerHTML = "Pause";
} else {
console.log("You must grant permission for the app to share your screen and use your microphone");
}
}
function pause() {
if (recorder != null) {
recorder.pause();
clearInterval(myInterval);
document.getElementById("recording").value = 0;
document.getElementById("record").innerHTML = "Continue Recording";
let timeLabel = document.getElementById("recordingTime").innerHTML;
document.getElementById("recordingTime").innerHTML = "" + recorder.state + ": " + timeLabel.substring(timeLabel.indexOf(":") + 1, timeLabel.length);
} else {
console.log("You must grant permission for the app to share your screen and use your microphone");
}
}
function stopRecording() {
if (recorder != null) {
if (recorder.state != "inactive") {
pause();
filename = prompt("Save As: (Do not include the dot file extension)", "video " + videoNumber);
if (filename != null) {
recorder.stop();
}
}
} else {
console.log("You must grant permission for the app to share your screen and use your microphone");
}
}
function countTime() {
seconds++;
let hours = Math.floor(seconds / 3600);
let minutes = Math.floor((seconds - (hours * 3600)) / 60);
let secs = seconds - (hours * 3600) - (minutes * 60);
let hourString = "";
if (hours < 10) {
hourString += "0";
}
hourString += hours;
let minuteString = "";
if (minuteString < 10) {
minuteString += "0";
}
minuteString += minutes;
let secString = "";
if (secs < 10) {
secString += "0";
}
secString += secs;
let timeString = hourString + ":" + minuteString + ":" + secString;
document.getElementById("recordingTime").innerHTML = "" + recorder.state + ": " + timeString;
}
</script>
</body>
</html>
The problem comes in with the trimming functionality (look mostly at the trueTrim() and the trimVideo() functions above). I can trim a video just fine and get a perfectly uncorrupted video if I select 0 as the start time of the trimmed video. However, if I want to trim a video to get the section from 10 s to 15 s or something like that, then I will get a video that either can't be played at all, or a video that lacks the visuals and starts playing the sounds after waiting for seconds. As can be seen above, I tried an approach of making sure at least some bytes of chunk 0 (the first blob in the chunks array from which the mp4 blob is created) are always present in the newChunks array (the array that is used to create trimmed mp4 blobs). I even tried an approach where I modified chunk 0 to have the first few bytes (which I believe to be the header) and then have the bytes from chunk , and then the rest of the newChunks array would just be populated with the rest of the chunks in range from start time to end time. However, even this creates an improper video.
I suppose that what I am trying to ask is this: What is it about chunk 0 (that first blob slice in the overall mp4 blob) that needs to be maintained for the video to work properly? How much of chunk 0's data do I need to maintain to be able to get a properly trimmed video?