Django and Ajax: Building a Django live recording application
John Owolabi Idogun
Posted on July 16, 2021
Motivation
Recently, I was working on the Q and A section of a web application. The requirements mandated that users should be provided the option of recording questions live in English or any other supported language. Not only that, the customer support centre should have the same privilege of responding with live recorded answers. While scurring the web for some solutions, I came across Recording audio in Django model but the response is somehow outdated. I decided to re-implement a working example using the technologies he suggested.
First off, it is assumed you are pretty much familiar with Django. Since we'll be using a lot of Ajax and JavaScript, you should have a working knowledge of JavaScript. Bulma CSS will be used for the presentation, though not required, familiarity with the framework is great.
Source code
The complete code for this article is on GitHub and can be accessed via:
Open up the created project in the text editor or IDE of choice (I stick with Visual Studio Code) and navigate to your project's settings.py file. In the file, locate INSTALLED_APPS and append the created application to it, like so:
# record > settings.py
...INSTALLED_APPS=["django.contrib.admin","django.contrib.auth","django.contrib.contenttypes","django.contrib.sessions","django.contrib.messages","django.contrib.staticfiles",#Add the created app
"core.apps.CoreConfig",]...
Create a urls.py in the core app folder and paste the following in:
Navigate to your project's urls.py file and make it look like this:
# record > urls.py
fromdjango.confimportsettingsfromdjango.conf.urls.staticimportstaticfromdjango.contribimportadminfromdjango.urlsimportpathfromdjango.urls.confimportincludeurlpatterns=[path("admin/",admin.site.urls),path("",include("core.urls",namespace="core")),# this adds a namespace to our core app using its urls.py file
]ifsettings.DEBUG:urlpatterns+=static(settings.STATIC_URL,document_root=settings.STATIC_ROOT)urlpatterns+=static(settings.MEDIA_URL,document_root=settings.MEDIA_ROOT)
instruct django to serve these files(static and media) when DEBUG=True (i.e during development)
Step 4 - Configure templates, static and media directories
Since we'll be using a lot of templates, static and media files, configure the directories django should look at for them. Don't forget to create these folders at the root of your project.
...TEMPLATES=[{"BACKEND":"django.template.backends.django.DjangoTemplates","DIRS":[BASE_DIR/"templates"],#Add template directory her
"APP_DIRS":True,"OPTIONS":{"context_processors":["django.template.context_processors.debug","django.template.context_processors.request","django.contrib.auth.context_processors.auth","django.contrib.messages.context_processors.messages",],},},]...STATIC_URL="/static/"STATICFILES_DIRS=(BASE_DIR/"static",)STATIC_ROOT=BASE_DIR/"staticfiles"STATICFILES_FINDERS=["django.contrib.staticfiles.finders.FileSystemFinder","django.contrib.staticfiles.finders.AppDirectoriesFinder",]MEDIA_URL="/media/"MEDIA_ROOT=BASE_DIR/"media"...
Create the templates, static and media directories.
(env) āāā(sirneij@sirneij)-[~/Documents/Projects/Django/django_record]
āā$[sirneij@sirneij django_record]$ mkdir-p templates static media
Step 5 ā Add the index view
To test our setup so far, navigate to your app's views.py and append the following:
It is a simple Function Based View(FBV) that renders a simple yet-to-be-created template index.html which is found in the core directory of the templates directory. Before creating this directory and html file, let's link it up to the urls.py file.
Now, create the core subdirectory in the templates folder and append index.html to it. But before then, let's work on the layout file for the entire application. I name it _base.html.
This _base.html was copied from Bulma CSS Starter template and some modifications were made. Notice that I am not using Bulma CSS CDN. I prefer serving my static files locally to reduce network calls.
Now to index.html:
<!--templates > core > index.html --><!--inherits the layout-->{%extends'_base.html'%}<!--passes the page title-->{%blocktitle%}{{page_title}}{%endblocktitle%}<!--content starts-->{%blockcontent%}<sectionclass="section"><divclass="container"><h1class="title">Hello World</h1><pclass="subtitle">My first website with <strong>Bulma</strong>!</p></div></section>{%endblockcontent%}
The comments say it all.
It's time to test it out! Open your terminal and runserver!
(env) āāā(sirneij@sirneij)-[~/Documents/Projects/Django/django_record]
āā$[sirneij@sirneij django_record]$ python manage.py runserver
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
You have 18 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.
July 16, 2021 - 19:09:00
Django version 3.2.5, using settings 'record.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
Neglect the warnings for now. Open up your browser and visit http://127.0.0.1:8000/.
From now on, I won't talk much about HTML and CSS.
Step 6 ā Create a model and view logic
Now to the first half of the real deal. Let's create a simple model to hold the recorded audios and add a view logic for exposing a POSTAPI for recording so that Ajax can consume it later on.
The model is just a normal one. I am always fond of overriding the default BigAutoField Django gives id. I prefer a UUID field. Aside from that, the table has only two fields: voice_records and language which is optional. Our recordings will be stored in the records subdirectory of the media directory.
The record function exposes the creation of the recording and stores it thereafter. For the detail view, record_detail handles getting only a single recording and our index lists all available recordings in the database.
Let's reflect all these changes in our app's urls.py file.
You should be greeted with something that looks like:
Operations to perform:
Apply all migrations: admin, auth, contenttypes, core, sessions
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying admin.0003_logentry_add_action_flag_choices... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying auth.0009_alter_user_last_name_max_length... OK
Applying auth.0010_alter_group_name_max_length... OK
Applying auth.0011_update_proxy_permissions... OK
Applying auth.0012_alter_user_first_name_max_length... OK
Applying core.0001_initial... OK
Applying sessions.0001_initial... OK
Step 7 - Introducing videojs-record and ajax
It's time to record something. To do this, we need a bunch of .js files and a couple of .css. jQuery will be needed too for ajax. In the complete version of the project, all these files are included but below are some excerpts:
<!-- templates > _base.html -->{%loadstatic%}<!DOCTYPE html><html><head><metacharset="utf-8"/><metaname="viewport"content="width=device-width, initial-scale=1"/><title>Django Ajax - {%blocktitle%}{%endblocktitle%}</title><linkrel="stylesheet"href="{%static'assets/css/bulma.min.css'%}"/>{%blockcss%}{%endblockcss%}</head><body><!--header-->{%include'includes/_header.html'%}<!--content-->{%blockcontent%}{%endblockcontent%}<!-- js--><script src="{%static'assets/js/jquery.min.js'%}"></script><script>consttriggerModal=document.getElementById("triggerModal");triggerModal.style.display="none";constcsrftoken=$("[name=csrfmiddlewaretoken]").val();if (csrftoken){functioncsrfSafeMethod(method){// these HTTP methods do not require CSRF protectionreturn/^(GET|HEAD|OPTIONS|TRACE)$/.test(method);}$.ajaxSetup({beforeSend:function (xhr,settings){if (!csrfSafeMethod(settings.type)&&!this.crossDomain){xhr.setRequestHeader("X-CSRFToken",csrftoken);}},});}</script>{%blockjs%}{%endblockjs%}</body></html>
This portion:
...constcsrftoken=$("[name=csrfmiddlewaretoken]").val();if (csrftoken){functioncsrfSafeMethod(method){// these HTTP methods do not require CSRF protectionreturn/^(GET|HEAD|OPTIONS|TRACE)$/.test(method);}$.ajaxSetup({beforeSend:function (xhr,settings){if (!csrfSafeMethod(settings.type)&&!this.crossDomain){xhr.setRequestHeader("X-CSRFToken",csrftoken);}},});}...
helps get the csrf tokens from the form we'll be processing later without explicitly including its value in all ajaxPOST calls. This is pretty handy in applications with many forms which will be processed with ajax.
Now to templates/core/record.html.
<!-- templates > core > record.html --><!--inherits the layout-->{%extends'_base.html'%}<!--static-->{%loadstatic%}<!--title-->{%blocktitle%}{{page_title}}{%endblocktitle%}<!--additional css-->{%blockcss%}<linkhref="{%static'assets/css/video-js.css'%}"rel="stylesheet"/><linkhref="{%static'assets/css/all.min.css'%}"rel="stylesheet"/><linkhref="{%static'assets/css/videojs.wavesurfer.min.css'%}"rel="stylesheet"/><linkhref="{%static'assets/css/videojs.record.css'%}"rel="stylesheet"/><style>/* change player background color */#createQuestion{background-color:#198754;}</style>{%endblockcss%}<!--content-->{%blockcontent%}<sectionclass="section"><divclass="container"><divclass="columns"><divclass="column is-offset-4 is-4"><h1class="title">Record audio</h1><articleclass="message is-success"id="alert"><divclass="message-header"><p>Recorded successfully!</p><buttonclass="delete"aria-label="delete"></button></div><divclass="message-body">
You have successfully recorded your message. You can now click on
the Submit button to post it.
</div></article><formmethod="POST"enctype="multipart/form-data">{%csrf_token%}<divclass="field"><divclass="control has-icons-left has-icons-right"><inputclass="input"type="text"placeholder="Language"/><spanclass="icon is-left"><iclass="fas fa-language"></i></span><spanclass="icon is-right"><iclass="fas fa-check"></i></span></div><divclass="control has-icons-left has-icons-right"><audioid="recordAudio"class="video-js vjs-default-skin"></audio></div></div></form></div></div></div></section>{%endblockcontent%}<!--additional js-->{%blockjs%}<script src="{%static'assets/js/video.min.js'%}"></script><script src="{%static'assets/js/RecordRTC.js'%}"></script><script src="{%static'assets/js/adapter-latest.js'%}"></script><script src="{%static'assets/js/wavesurfer.js'%}"></script><script src="{%static'assets/js/wavesurfer.microphone.min.js'%}"></script><script src="{%static'assets/js/videojs.wavesurfer.min.js'%}"></script><script src="{%static'assets/js/videojs.record.min.js'%}"></script><script src="{%static'assets/js/browser-workaround.js'%}"></script>{%endblockjs%}
All these additional files were included in the official audio-only example of videojs-record library. Visiting http://localhost:8000/record/ should look like this:
Step 8 - Adding recording and ajax calls
To have the real feeling of recording, let's do the real thing - recording!
Create a new .js file in the js subdirectory of your static files directory. I call it real.recording.js. Populate it with the following:
// First lets hide the messagedocument.getElementById("alert").style.display="none";// Next, declare the options that will passed into the recording constructorconstoptions={controls:true,bigPlayButton:false,width:600,height:300,fluid:true,// this ensures that it's responsiveplugins:{wavesurfer:{backend:"WebAudio",waveColor:"#f7fff7",// change the wave color here. Background color was set in the css aboveprogressColor:"#ffe66d",displayMilliseconds:true,debug:true,cursorWidth:1,hideScrollbar:true,plugins:[// enable microphone pluginWaveSurfer.microphone.create({bufferSize:4096,numberOfInputChannels:1,numberOfOutputChannels:1,constraints:{video:false,audio:true,},}),],},record:{audio:true,// only audio is turned onvideo:false,// you can turn this on as well if you prefer video recording.maxLength:60,// how long do you want the recording?displayMilliseconds:true,debug:true,},},};// apply audio workarounds for certain browsersapplyAudioWorkaround();// create player and pass the the audio id we created thenvarplayer=videojs("recordAudio",options,function (){// print version information at startupvarmsg="Using video.js "+videojs.VERSION+" with videojs-record "+videojs.getPluginVersion("record")+", videojs-wavesurfer "+videojs.getPluginVersion("wavesurfer")+", wavesurfer.js "+WaveSurfer.VERSION+" and recordrtc "+RecordRTC.version;videojs.log(msg);});// error handlingplayer.on("deviceError",function (){console.log("device error:",player.deviceErrorCode);});player.on("error",function (element,error){console.error(error);});// user clicked the record button and started recordingplayer.on("startRecord",function (){console.log("started recording!");});// user completed recording and stream is availableplayer.on("finishRecord",function (){constaudioFile=player.recordedData;console.log("finished recording: ",audioFile);$("#submit").prop("disabled",false);document.getElementById("alert").style.display="block";});
Your templates/core/record.html should now look like:
<!--inherits the layout-->{%extends'_base.html'%}<!--static-->{%loadstatic%}<!--title-->{%blocktitle%}{{page_title}}{%endblocktitle%}<!--additional css-->{%blockcss%}<linkhref="{%static'assets/css/video-js.css'%}"rel="stylesheet"/><linkhref="{%static'assets/css/all.min.css'%}"rel="stylesheet"/><linkhref="{%static'assets/css/videojs.wavesurfer.min.css'%}"rel="stylesheet"/><linkhref="{%static'assets/css/videojs.record.css'%}"rel="stylesheet"/><style>/* change player background color */#recordAudio{background-color:#3e8ed0;}</style>{%endblockcss%}<!--content-->{%blockcontent%}<sectionclass="section"><divclass="container"><divclass="columns"><divclass="column is-offset-4 is-4"><h1class="title">Record audio</h1><articleclass="message is-success"id="alert"><divclass="message-header"><p>Recorded successfully!</p><buttonclass="delete"aria-label="delete"></button></div><divclass="message-body">
You have successfully recorded your message. You can now click on
the Submit button to post it.
</div></article><formmethod="POST"enctype="multipart/form-data">{%csrf_token%}<divclass="field"><divclass="control has-icons-left has-icons-right"><inputclass="input"type="text"placeholder="Language"/><spanclass="icon is-left"><iclass="fas fa-language"></i></span><spanclass="icon is-right"><iclass="fas fa-check"></i></span></div><divclass="control has-icons-left has-icons-right"style="margin-top: 1rem"><audioid="recordAudio"class="video-js vjs-default-skin"></audio></div><divclass="control"style="margin-top: 1rem"><buttonclass="button is-info"id="submit">Submit</button></div></div></form></div></div></div></section>{%endblockcontent%}<!--additional js-->{%blockjs%}<script src="{%static'assets/js/video.min.js'%}"></script><script src="{%static'assets/js/RecordRTC.js'%}"></script><script src="{%static'assets/js/adapter-latest.js'%}"></script><script src="{%static'assets/js/wavesurfer.js'%}"></script><script src="{%static'assets/js/wavesurfer.microphone.min.js'%}"></script><script src="{%static'assets/js/videojs.wavesurfer.min.js'%}"></script><script src="{%static'assets/js/videojs.record.min.js'%}"></script><script src="{%static'assets/js/browser-workaround.js'%}"></script><script src="{%static'assets/js/real.recording.js'%}"></script>{%endblockjs%}
Ajax proper:
...// Give event listener to the submit button$("#submit").on("click",function (event){event.preventDefault();letbtn=$(this);// change the button text and disable itbtn.html("Submitting...").prop("disabled",true).addClass("disable-btn");// create a new File with the recordedData and its nameconstrecordedFile=newFile([player.recordedData],`audiorecord.webm`);// grabs the value of the language fieldconstlanguage=document.getElementById("language").value;// initializes an empty FormDataletdata=newFormData();// appends the recorded file and language valuedata.append("recorded_audio",recordedFile);data.append("language",language);// post url endpointconsturl="";$.ajax({url:url,method:"POST",data:data,dataType:"json",success:function (response){if (response.success){document.getElementById("alert").style.display="block";window.location.href="/";}else{btn.html("Error").prop("disabled",false);}},error:function (error){console.error(error);},cache:false,processData:false,contentType:false,});});
Small update
The ajax code might fail or give undesirable output in Firefox browsers if the event argument isn't passed in the callback function, followed by the first line event.preventDefault();.
That's it! Such a long piece. Do you have some suggestions? Kindly drop them in the comment section.
Outro
Enjoyed this article? I'm a Software Engineer and Technical Writer actively seeking new opportunities, particularly in areas related to web security, finance, healthcare, and education. If you think my expertise aligns with your team's needs, let's chat! You can find me on LinkedIn and Twitter.
If you found this article valuable, consider sharing it with your network to help spread the knowledge!