Issue
In a nutshell
How can I use JavaScript to get a blob of video data to a Django view through a Django form?
Background
I'm building a feature to record a webcam video. I've successfully figured this part out. I can use JavaScript to stream the user's camera and mic, record the stream, turning it into a blob, and then I'm able to turn the blob into a file. However, I don't want a file because I need users' videos to simply be sent to my backend, and I don't want users to interact with a video file (and I just learned that you can't programmatically assign <input type='file'>
elements a file because of security reasons). This is why I'm looking to just send the binary data.
What I've Tried (and what the result was)
- Like I said, I tried using a FileField in my Django form (but cuz inherent web security restrictions, no worky). Having users download and upload the file themselves does not work with project requirements.
- I've tried using a JSONField for my Django form and submitting the binary in JSON format. Perhaps this could still work, but I can't get the binary data into JSON format. With everything I've tried so far, value in my JSON that should have the blob ends up being either an empty string,
undefined
, or a string like this ->"data:"
.
Code
JavaScript
// This function is what MediaRecorder uses to give data to "recordedChunks"
const handleDataAvailable = function(event) {
if (event.data.size > 0) {
recordedChunks.push(event.data);
} else {
// …
}
}
The problem lies in the next step (stop recording, make blob, and get blob to input
element). I will show each variation of this function, explaining beforehand what the value of base64data
ended up being:
console.log(base64data);
= undefined
const stopCapture = function() {
mediaRecorder.stop();
const blob = new Blob(recordedChunks, {type: "video/webm",});
// Convert the Blob to base64 and then JSON format to submit with form
const reader = new FileReader();
reader.onload = function () {
const base64data = reader.result.split(',')[1];
console.log(base64data);
const jsonData = JSON.stringify({ videoBlob: base64data });
// Set the value of the input element to the base64-encoded blob
jsonInput.value = jsonData;
};
reader.readAsDataURL(blob);
}
console.log(base64data);
= data:
Changed const base64data = reader.result.split(',')[1];
to const base64data = reader.result;
.
console.log(base64data);
= an empty string
//...after blob is created
// Convert the Blob to base64 and then JSON format to submit with form
const reader = new FileReader();
reader.onload = function () {
const arrayBuffer = reader.result;
// Convert array buffer to base64
const base64data = arrayBufferToBase64(arrayBuffer);
// Create a JSON-formatted string with the base64-encoded blob data
const jsonData = JSON.stringify({ videoBlob: base64data });
// Set the value of the hidden input to the JSON representation
blobInput.value = jsonData;
};
reader.readAsArrayBuffer(blob);
}
// Function to convert array buffer to base64
function arrayBufferToBase64(arrayBuffer) {
const uint8Array = new Uint8Array(arrayBuffer);
const binaryString = uint8Array.reduce((acc, byte) => acc + String.fromCharCode(byte), '');
return btoa(binaryString);
}
Other Ideas
- Maybe it would be best to not use a Django form? If I did this, then I would submit the form data through JavaScript itself to a separate view in Django (using
formData()
andfetch()
). However, I'd rather not do this because I like the built-in security that Django forms provide. - Perhaps it's best to send the data to my Django backend (a separate view) before turning it into a blob somehow?
Django form and view code
Including this just in case there are suggestions about my overall workflow since my end goal is to go from the user recording a video to my backend sending that video to a 3rd party storage service.
forms.py
from django import forms
class VidFileUploadForm(forms.Form):
vidBlob = forms.JSONField(widget=forms.HiddenInput)
name = forms.CharField(required=True)
descrip = forms.CharField(required=True)
views.py
class VidDemoTwoView(FormMixin, TemplateView, ProcessFormView):
"""View class for experimental video recording view"""
template_name = 'evaluations/vid_demo2.html'
form_class = VidFileUploadForm
success_url = reverse_lazy('view-vid-demo')
def form_valid(self, form):
vidBlob = form.cleaned_data['vidBlob']
name = form.cleaned_data['name']
description = form.cleaned_data['descrip']
#logic to turn video blob into video file and then upload to 3rd party storage service
return super().form_valid(form)
Solution
I got the approach to work. My problem was the MediaRecorder wasn't finished doing its thing before I was attempting to create the blob. Here's my full script so you can see how I fixed it (creating the recordingPromise
and making stopCapture an async
function were key (so that I could await
the recordingPromise before trying to create the blob)):
JavaScript
/* -------------------
-------- Capture -------
------------------- */
const vidDisplay = document.querySelector("#vidDisplay");
const startRecordBtn = document.querySelector("#startRecordBtn");
const stopRecordBtn = document.querySelector("#stopRecordBtn");
const sendBtn = document.querySelector("#sendBtn");
const blobInput = document.querySelector("#id_vidBlob");
const resultDisplay = document.querySelector("#result");
/* -------------------------
--------- Variables ----------
--------------------------- */
// gotta have the chunks
const recordedChunks = [];
// User media constraints
const constraints = {
audio: true,
video: {
width: 640,
height: 360
}
};
// declare stream globally
let stream;
// declare mediaRecorder globally
let mediaRecorder;
// declare recordingPromise globally
let recordingPromise;
// Recorder options
const recorderOptions = {
mimeType: "video/webm; codecs=vp9",
audioBitsPerSecond: 8000,
videoBitsPerSecond: 156250,
};
/* -------------------------
--------- Functions ----------
--------------------------- */
// Function for starting screen capture
const startCapture = async function() {
try {
stream = await navigator.mediaDevices.getUserMedia(constraints);
vidDisplay.srcObject = stream;
// create media recorder
mediaRecorder = new MediaRecorder(stream, recorderOptions);
mediaRecorder.ondataavailable = handleDataAvailable;
// start up recorder
mediaRecorder.start();
// Create a promise to resolve when the recording is stopped
recordingPromise = new Promise((resolve) => {
mediaRecorder.onstop = resolve;
});
} catch (err) {
console.error(err);
}
}
// Function for recorder
const handleDataAvailable = function(event) {
console.log("data is available");
if (event.data.size > 0) {
recordedChunks.push(event.data);
} else {
// …
}
}
// Function for stopping screen capture
const stopCapture = async function() {
let tracks = vidDisplay.srcObject.getTracks();
tracks.forEach((track) => track.stop());
vidDisplay.srcObject = null;
// stop ye recorder
mediaRecorder.stop();
await recordingPromise;
const blob = new Blob(recordedChunks, {type: "video/webm",}); // create blob from recordedChunks
// Convert the Blob to base64 and then JSON format to submit with form
const reader = new FileReader();
reader.onloadend = function () {
try {
const base64data = reader.result.split(',')[1];;
console.log(base64data);
// Create a JSON-formatted string with the base64-encoded blob data
const jsonData = JSON.stringify({ videoBlob: base64data });
// Set the value of the hidden input to the base64-encoded blob
blobInput.value = jsonData;
} catch (error) {
console.error('Error during FileReader operation:', error);
}
};
// read video data
reader.readAsDataURL(blob);
}
/* -------------------------
--------- Event Listeners ----------
--------------------------- */
startRecordBtn.addEventListener("click", startCapture);
stopRecordBtn.addEventListener("click", stopCapture);
This made it so I could easily submit the blob in the JSONField that I created in my form, and it's proving pretty easy to work with that in the view so that I can upload the video file to a 3rd party storage service.
Answered By - Devin Gilbert
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.