Search code examples
javascriptdjangovideodjango-formsblob

How can you use JavaScript to send a video blob to a Django view using a Django form?


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() and fetch()). 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.