'How do I upload a webcam image using JavaScript ajax to a Django site?

I am creating a livestreaming site using Django and I need to upload an image using an ajax post request to a model form on the django site. I am working with the following code:

Models:

from django.db import models
from django.contrib.auth.models import User

class Camera(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True, related_name='camera')
    image = models.ImageField(upload_to='live/', null=True, blank=True)

Views:

from .models import Camera
from .forms import CameraForm

@login_required
@csrf_exempt
def golive(request):
    cameras = Camera.objects.filter(user=request.user)
    camera = None
    if cameras.count() == 0:
        camera = Camera.objects.create(user=request.user)
        camera.save()
    else:
        camera = cameras.first()
    if request.method == 'POST':
        print(request.FILES)
        form = CameraForm(request.POST, request.FILES, instance=camera)
        if form.is_valid():
            form.save()
        print("Working")
        return redirect('live:live')
    return render(request, 'live/golive.html', {'object': request.user.camera, 'form': CameraForm()})

@login_required
def live(request, username):
    profile = get_object_or_404(Profile, user__username=username, identity_verified=True, vendor=True)
    cameras = Camera.objects.filter(user=profile.user)
    return render(request, 'live/live.html', {'profile': profile, 'camera': cameras.first()})

Templates:

{% extends 'base.html' %}
{% block content %}
<h1>Go live</h1>
<div id="container">
<video autoplay="true" id="video">
</video>
<canvas id="canvas" width="1028" height="728">
</canvas>
<button id="capture"></button>
</div>
{% endblock %}
{% block javascript %}
var video = document.getElementById('video');
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
function capture(){
    ctx.drawImage(video, 0, 0, 1024, 728);
    ctx.save();
    canvas.toBlob(function(blob){
        console.log(blob);
    var fileOfBlob = new File([blob], 'camera.png');
        var formData = new FormData();
    formData.append('image', fileOfBlob, 'camera.png');
        $.ajax({
            url: window.location.pathname,
            type: 'POST',
            data: formData,
            success: function (response) {
        console.log("Captured image.")
            },
            cache: false,
            contentType: false,
            processData: false
        });
    },'image/png');
}
setInterval(capture, 10000);
function startup() {
  navigator.mediaDevices.getUserMedia({video: true, audio: false})
  .then(function(stream) {
    video.srcObject = stream;
    video.play();
  })
  .catch(function(err) {
    console.log("An error occurred: " + err);
  });
}
startup();
{% endblock %}

Forms:

from django import forms
from .models import Camera

class CameraForm(forms.ModelForm):
    class Meta:
        model = Camera
        fields = ('image',)

The webcam renders on the page just fine but when I try to render {{ camera.image.url }} I see that the image has not been uploaded. How do I upload a blob image using ajax to a django site? Is there something I am missing? This is the error I am getting:

django.http.multipartparser.MultiPartParserError: Invalid boundary in multipart: None


Solution 1:[1]

This is sort of out of the box, but this is the solution I came up with. I wrote my own basic codec using only blobs, which uses a setInterval to collect images at a short interval and uploads them to the server every 5 seconds. I then load the images and split them, using another setInterval to display them. This is the code:

Models:

from django.db import models
from django.utils import timezone
from django.contrib.auth.models import User


class Camera(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True, related_name='camera')
    src = models.TextField(default="", null=True, blank=True)
    last_frame = models.DateTimeField(default=timezone.now)

Views:

from django.shortcuts import render, redirect, get_object_or_404
from django.urls import reverse
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from users.models import Profile
from django.contrib import messages
from django.views.decorators.cache import never_cache
from django.views.decorators.csrf import csrf_exempt
import datetime
from django.utils import timezone
from django.core.paginator import Paginator
from django.utils.decorators import method_decorator
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.contrib.auth.decorators import user_passes_test
from feed.tests import identity_verified
from vendors.tests import is_vendor
from django.core.exceptions import PermissionDenied
from django.http import StreamingHttpResponse
from django.http import HttpResponse
from django.core.files.storage import FileSystemStorage
import uuid
import imghdr
import os
from django.conf import settings
from .models import Camera
from django.core.files import File
from django.core.files.base import ContentFile
from django.core.files.temp import NamedTemporaryFile
from django import forms
import traceback
from urllib.request import urlopen

@login_required
@user_passes_test(identity_verified, login_url='/verify/', redirect_field_name='next')
@user_passes_test(is_vendor)
@csrf_exempt
def golive(request):
    cameras = Camera.objects.filter(user=request.user)
    camera = None
    if cameras.count() == 0:
        camera = Camera.objects.create(user=request.user)
        camera.save()
    else:
        camera = cameras.first()
    if request.method == 'POST':
        try:
            p = ''
            for key, value in request.POST.items():
                value = value.split('\n')
                p = p + value[2] + value[3] + '*'
            p = p.replace(' ', '+')
            camera.last_frame = timezone.now()
            camera.src = p
            camera.save()
        except:
            print(traceback.format_exc())
        return HttpResponse(status=200)
    return render(request, 'live/golive.html', {'object': request.user.camera})

@login_required
@user_passes_test(identity_verified, login_url='/verify/', redirect_field_name='next')
@csrf_exempt
def live(request, username):
    profile = get_object_or_404(Profile, user__username=username, identity_verified=True, vendor=True)
    cameras = Camera.objects.filter(user=profile.user)
    return render(request, 'live/live.html', {'profile': profile, 'camera': cameras.first()})

@login_required
@user_passes_test(identity_verified, login_url='/verify/', redirect_field_name='next')
@csrf_exempt
def last_frame(request, username):
    profile = get_object_or_404(Profile, user__username=username, identity_verified=True, vendor=True)
    cameras = Camera.objects.filter(user=profile.user)
    return render(request, 'live/lastframe.html', {'profile': profile, 'camera': cameras.first()})


@login_required
@user_passes_test(identity_verified, login_url='/verify/', redirect_field_name='next')
@csrf_exempt
def frame(request, username):
    profile = get_object_or_404(Profile, user__username=username, identity_verified=True, vendor=True)
    cameras = Camera.objects.filter(user=profile.user)
    return render(request, 'live/liveframe.html', {'profile': profile, 'camera': cameras.first()})

Templates:

-- The template for viewing the live feed:
{% extends 'base.html' %}
{% block content %}
<h1>@{{ profile.user.username }}'s Live Feed</h1>
<div id="lastFrame"></div>
<div id="container">
<canvas id="videoCanvas" width="512" height="364">
</canvas>
<iframe src="/chat/{{ profile.user.username }}/?hidenavbar=t" width="100%" height="500px">
</iframe>
{% endblock %}
{% block javascript %}
var canvas = document.getElementById("videoCanvas");
var lastFrame = document.getElementById("lastFrame");
if(window.innerWidth > window.innerHeight) {
    canvas.width = parseInt(window.innerWidth * 0.90);
    canvas.height = parseInt(canvas.width * 364/512);
} else {
    canvas.width = parseInt(window.innerWidth * 0.60);
    canvas.height = parseInt(canvas.width * 364/512);
}
let xhr = new XMLHttpRequest();
let xhr2 = new XMLHttpRequest();
function loadListener2(){
        if (xhr2.readyState === xhr2.DONE) {
        if(!xhr2.responseText.includes("Error 500")){
            lastFrame.innerHTML = xhr2.responseText;
            }
    }
}
function loadListener(){
        if (xhr.readyState === xhr.DONE) {
            var data = xhr.responseText.substring(0, xhr.responseText.length)
        data = data.replace(' ', '+');
        data = data.replace(' ', '+');
        data = data.replace('\n','');
            if(!data.includes("Error 500")){
                var images = data.split("*");
        var count = 0;
        var interval = setInterval(function(){
                    canvas.renderImage(images[count]);
            count++;
        }, {{ record_interval }});
        setTimeout(function(){
            clearInterval(interval);
        }, 5000);
        }
        }
}
function render() {
        xhr.addEventListener("load", loadListener);
        xhr.open("POST", window.location.pathname + "frame/", true);
        xhr.send(null);
    xhr2.addEventListener("load", loadListener2);
        xhr2.open("POST", window.location.pathname + "frame/last/", true)
        xhr2.send(null);
}
setInterval(render, 5000);
HTMLCanvasElement.prototype.renderImage = function(blob){
  var ctx = this.getContext('2d');
  var img = new Image();
  img.onload = function(){
    ctx.drawImage(img, 0, 0, canvas.getBoundingClientRect().width, canvas.getBoundingClientRect().height);
    ctx.save();
  };
  img.src = blob;
};
render();
{% endblock %}

-- The template for transmitting it
{% extends 'base.html' %}
{% block content %}
<h1>Go live</h1>
<div id="container">
<video autoplay="true" id="video" width="100%">
</video>
<canvas id="canvas" width="512" height="364" style="visibility: hidden;">
</canvas>
</div>
{% endblock %}
{% block javascript %}
var video = document.getElementById('video');
var canvas = document.getElementById('canvas');
var image = document.getElementById('image');
var form = document.getElementById('input-form');
var ctx = canvas.getContext('2d');
var data;
function drawImage(){
    ctx.drawImage(video, 0, 0, 512, 364);
    ctx.save();
    data = canvas.toDataURL('image/png').replace(' ', '+')
    data = data.replace('\n','');
}
function capture(){
    //console.log(data);
    var dataArray = "";
    var formData = new FormData();
    var count = 0;
    var recordInterval = setInterval(function(){
        drawImage();
        dataArray = dataArray + data + "*";
        count++;
    }, {{ record_interval }});
    setTimeout(function(){
      formData.append("image", new Blob([dataArray], {'type': 'image/png'}), "image.png");
      clearInterval(recordInterval);        
      $.ajax({
        url: window.location.pathname,
        type: 'POST',
        data: formData,
        success: function (response) {
            console.log("Captured image.")
        },
        cache: false,
        processData: false
      });
    }, 5000);
}
setInterval(capture, 5000);
function startup() {
  navigator.mediaDevices.getUserMedia({video: true, audio: false})
  .then(function(stream) {
    video.srcObject = stream;
    video.play();
  })
  .catch(function(err) {
    console.log("An error occurred: " + err);
  });
}
startup();
{% endblock %}

I know I'm using the .replace several times, but this is because the image needs to be in URL format so I need to replace newlines with '', spaces with +, and then the blob will render correctly. Pasting a printed blob into the browser to test works well too! But for best practice, I am using the replace in multiple places to format the blob correctly.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Jasper Holton