<?xml version="1.0" encoding="utf-8"?>
    <feed xmlns="http://www.w3.org/2005/Atom">
     <title>BigBinary Blog</title>
     <link href="https://www.bigbinary.com/feed.xml" rel="self"/>
     <link href="https://www.bigbinary.com/"/>
     <updated>2026-06-08T05:23:32+00:00</updated>
     <id>https://www.bigbinary.com/</id>
     <entry>
       <title><![CDATA[Universal playback and streaming support using MP4 and Range request headers]]></title>
       <author><name>Unnikrishnan KP</name></author>
      <link href="https://www.bigbinary.com/blog/mp4_transmuxing_and_streaming_support-loom-alternative-part-3"/>
      <updated>2024-03-17T12:00:00+00:00</updated>
      <id>https://www.bigbinary.com/blog/mp4_transmuxing_and_streaming_support-loom-alternative-part-3</id>
      <content type="html"><![CDATA[<p>This is part 3 of our blog on how we are building<a href="https://www.neeto.com/neetorecord">NeetoRecord</a>, a Loom alternative. Here are<a href="https://www.bigbinary.com/blog/build-web-based-screen-recorder-loom-alternative-part-1">part 1</a>and<a href="https://www.bigbinary.com/blog/persistant-storage-for-recordings-in-s3-loom-alternative-part-2">part 2</a>.</p><p>In Part 1 of our blog, we uploaded the recording from the browser to S3 insmall parts and stitched them together to get the final WEBM video file. Wecould use this WEBM file to share our recording with our audience, but it has afew drawbacks:</p><ol><li><p>WEBM is not universally supported. Though most modern browsers support WEBM,a few browsers, especially devices in the Apple ecosystem, do not play WEBMreliably.</p></li><li><p>Metadata for timestamps and duration are not present in WEBM videos. So,these videos are not &quot;seekable.&quot; It means these videos do not show the videolength, and we cannot move back and forth using the seek bar. The videostarts playing back from the beginning when the user tries to push the seekbar.</p></li></ol><p>Hence, we needed to convert the WEBM videos to a universally supported format tosolve the above problems. We chose MP4.</p><h2>MP4</h2><p>MP4 is a widely used multimedia file storage format for video storage andstreaming. It is an international standard that works with a vast range ofdevices. MP4 refers to the digital container file that acts as a wrapper aroundthe video, not the video itself. The video content within MP4 files is encodedwith MPEG-4, a common encoding standard.</p><p>We chose MP4 because:</p><ol><li>MP4 works with<a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video">HTML5 video player</a>.</li><li>Supports multiple streaming protocols.</li><li>Comprehensive support for user devices and browsers.</li></ol><h2>WEBM to MP4 conversion</h2><h3>AWS MediaConvert service</h3><p>Since our WEBM files were in an S3 bucket, our first idea was to use an AWSservice to do the WEBM to MP4 conversion. We configured<a href="https://aws.amazon.com/mediaconvert/">AWS Elemental MediaConvert service</a> andconnected it to our WEBM bucket. When a user uploads a WEBM file to the bucket,MediaConvert picks it up, converts it to MP4, and uploads it to a new bucket.</p><p>MediaConvert worked as expected, but we had to find another solution because:</p><ol><li>Cost - we found it too expensive for our use case.</li><li>Performance - It took a long time to do the conversion. While the smallerrecordings took about 20-30s, large ones took minutes. The time taken grewlinearly with the size of the WEBM file.</li></ol><h3>Manual transcoding using FFMPEG using AWS Lambda</h3><p>Converting WEBM to MP4 involves transcoding. Transcoding is the process ofchanging the audio/video codecs in a container file. Codecs are algorithms usedto encode and decode digital media data. Converting to MP4 would mean usingcodecs that are part of the MPEG-4 family. Eg: H.264 for video and AAC foraudio. <a href="https://ffmpeg.org">FFMPEG</a> is a popular open source tool that can beused for transcoding WEBM to MP4.</p><pre><code>ffmpeg -i input.webm -c:v libx264 -c:a aac  output.mp4</code></pre><ul><li><code>-c:v libx264</code> sets the video codec to libx264, which is a widely supportedH.264 codec.</li><li><code>-c:a aac</code> sets the audio codec to AAC, which is a commonly used audio codec.</li></ul><p>We could run FFMPEG on our web server and run the transcoding process. But thatwill not be easy to scale. So, we decided to use a serverless solution thatwould automatically scale. Since our input files were on AWS S3, AWS Lambda wasthe obvious choice.</p><p>We installed FFMPEG on <a href="https://aws.amazon.com/lambda/">AWS Lambda</a> using a<a href="https://docs.aws.amazon.com/lambda/latest/dg/chapter-layers.html">Layer</a> asdescribed in this<a href="https://aws.amazon.com/blogs/media/processing-user-generated-content-using-aws-lambda-and-ffmpeg">post</a>.</p><p>We configured our input S3 bucket (one to which WEBM was uploaded) to triggerLambda whenever a new file was uploaded. FFMPEG would then transcode WEBM to MP4and store the output in another S3 bucket.</p><p>This worked as expected. But performance was still a problem. Time taken wasproportional to the input file size and took longer than we found acceptable.</p><h3>Transmuxing instead of transcoding</h3><p>Transmuxing or stream copy is a fast process that doesn't involve re-encodingbut instead directly copies the existing audio and video streams into a newcontainer format. This approach works well when the codecs used in the inputfile (WebM) are compatible with the output container format (MP4).</p><p>Popular browsers like Chrome, Brave, Safari etc. use the H264 codec for videoencoding. This is compatible with MP4. So transmuxing works flawlessly. ButFirefox uses the VP8 or VP9 codec, which is incompatible with MP4. Since we wereplanning to build a Chrome extension for NeetoRecor,d we only needed to worryabout Chrome and we could ignore Firefox users for now.</p><pre><code>ffmpeg -i input.webm -c:v copy -c:a copy output.mp4</code></pre><p>We modified the ffmpeg command as shown above. It now uses the <code>-c:v copy</code> and<code>-c:a copy</code> options, which copies the video and audio from the input file to theoutput file without re-encoding. MP4 conversion now become extremely fast, andthe time taken did not increase significantly with the size of the input file.</p><h2>Streaming</h2><p>Now that we successfully generated MP4 files, it was time to think of deliveringthe file efficiently to the client (browser) for playback. We had two problemsto solve:</p><ol><li><p>S3 is a storage service. It is not suitable for content delivery.</p><ul><li>Relatively high data transfer costs.</li><li>Storage is in one geographical region, resulting in slower delivery overthe network.</li></ul></li><li><p>Video files are large in size. Downloading the entire file and then playingit back is not efficient in terms of speed and data transfer. We needed tofind a way to allow streaming of the files. ie. deliver chunks of data as andwhen it was needed by the client.</p></li></ol><h3>Cloudfront as CDN</h3><p>CloudFront is a content delivery network (CDN) service provided by AWS. It canbe used as a CDN for S3, and this combination is a common architecture fordistributing content globally with low latency and high transfer speeds.</p><p>We created a CloudFront distribution, which is connected to our MP4 bucket. Oncethe distribution is deployed, we can access the MP4 files using the CloudFrontdomain name. When users request content through CloudFront, CloudFront checksits cache for the requested content. If the content is in the cache and is stillvalid (based on cache-control headers), CloudFront serves the content directlyfrom its edge locations, reducing latency. If the content is not in the cache oris expired, CloudFront retrieves the content from the S3 bucket, caches it, andserves it to the user. This helps reduce the load on our S3 bucket and improvesthe performance of content delivery.</p><h3>The HTTP Range request header</h3><p>HTTP Range requests allow clients to request specific portions of a file from aserver. This feature enables users to stream or download only parts of the filethey need, reducing bandwidth usage and improving user experience. At first, theclient could request the range for the beginning of the video file, and then, as theplayback proceeded, request subsequent parts. If the user moves back and forththe video using the seek bar, corresponding ranges can be requested.</p><pre><code>GET /example.mp4 HTTP/1.1Host: example.comRange: bytes=5000-9999</code></pre><p><code>Range: bytes=5000-9999</code> is the Range header indicating the specific bytes theclient wants to retrieve. In this case, the client requests bytes 5000 to 9999of the MP4 file. The numbering starts from zero, so byte 5000 means the 5001stbyte in the file.</p><p>Server responds with a <code>206</code> response (Partial content) with the requestsequence of bytes in the body. If the server does not support range requests,then it responds with a <code>200</code> along with the full content.</p><h4>Checking if the server supports Range requests</h4><p>We can perform a check by issuing a HEAD request to the server to see if theserver supports Range requests.</p><pre><code>curl -I http://abc.com/1.mp4</code></pre><p>If range requests are supported, then the server responds with a<code>Accept-Ranges: bytes</code> header.</p><pre><code>HTTP/1.1 200 OKAccept-Ranges: bytesContent-Length: 146515</code></pre><p>We did the test on our S3 bucket directly first, and then through Cloudfront.Both<a href="https://docs.aws.amazon.com/whitepapers/latest/s3-optimizing-performance-best-practices/use-byte-range-fetches.html">S3</a>and<a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/RangeGETs.html">Cloudfront</a>supports Range request headers.</p><p>MP4, as mentioned above, supports streaming. It has metadata to help the serverdeliver it in chunks as requested. The HTML5 video player supports progressivedownload automatically by making use of HTTP Range headers.</p><p>So now we have our video in a file format that supports streaming (MP4), webserver that supports Range headers (S3 and Cloudfront) and a client that usesRange headers for progressive download - all the ingredients needed to supportstreaming.</p>]]></content>
    </entry><entry>
       <title><![CDATA[Efficient uploading and persistent storage of NeetoRecord videos using AWS S3]]></title>
       <author><name>Unnikrishnan KP</name></author>
      <link href="https://www.bigbinary.com/blog/persistant-storage-for-recordings-in-s3-loom-alternative-part-2"/>
      <updated>2024-03-16T12:00:00+00:00</updated>
      <id>https://www.bigbinary.com/blog/persistant-storage-for-recordings-in-s3-loom-alternative-part-2</id>
      <content type="html"><![CDATA[<p>This is part 2 of our blog on how we are building<a href="https://www.neeto.com/neetorecord">NeetoRecord</a>, a Loom alternative. Here are<a href="https://www.bigbinary.com/blog/build-web-based-screen-recorder-loom-alternative-part-1">part 1</a>and<a href="https://www.bigbinary.com/blog/mp4_transmuxing_and_streaming_support-loom-alternative-part-3">part 3</a>.</p><p>In the previous blog, we learned how to use the Browser APIs to record the screenand generate a WEBM file. We now need to upload this file to persistent storageto have a URL to share our recording with our audience.</p><p>Uploading a large file all at once is time-consuming and prone to failure due tonetwork errors. The recording is generated in parts, each part pushed to anarray and joined together. So it would be ideal if we could upload these smallerparts as and when they are generated, and then join them together in the backend oncethe recording is completed. AWS's<a href="https://aws.amazon.com/s3/">Simple Storage Service (S3)</a> made a perfect fit asit provides cheap persistent storage, along with<a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/mpuoverview.html">Multipart Uploads</a>feature.</p><p>S3 Multipart Uploads allow us to upload large objects in parts. Rather thanuploading the entire object in a single operation, multipart uploads break itdown into smaller parts, each ranging from 5 MB to 5 GB. Once uploaded, theseparts are aggregated to form the complete object.</p><h2>Initialization</h2><p>The process begins with an initiation request to S3, where a unique upload ID isgenerated. This upload ID is used to identify and manage the individual parts ofthe upload.</p><pre><code>s3 = Aws::S3::Client.newresp = s3.create_multipart_upload({  bucket: bucket_name,  key: object_key})upload_id = resp.upload_id</code></pre><h2>Upload Parts</h2><p>Once the upload is initiated, we can upload the parts to S3 independently. Eachpart is associated with a sequence number and an ETag (Entity Tag), a checksumof the part's data.</p><p>Note that the minimum content size for a part is 5MB (There is no minimum size limiton the last part of your multipart upload). So we store the recording chunks inlocal storage until they are bigger than 5MB. Once we have a part greater than5MB, we upload it to S3.</p><pre><code>part_number = 1content = recordedChunksresp = s3.upload_part({  body: content,  bucket: bucket_name,  key: object_key,  upload_id: upload_id,  part_number: part_number})puts &quot;ETag for Part #{part_number}: #{resp.etag}&quot;</code></pre><h2>Completion</h2><p>Once all parts are uploaded, a complete multipart upload request is sent to S3,specifying the upload ID and the list of uploaded parts along with their ETagsand sequence numbers. S3 then assembles the parts into a single object andfinalizes the upload.</p><pre><code>completed_parts = [  { part_number: 1, etag: 'etag_of_part_1' },  { part_number: 2, etag: 'etag_of_part_2' },  ...  { part_number: N, etag: 'etag_of_part_N' },]resp = s3.complete_multipart_upload({  bucket: bucket_name,  key: object_key,  upload_id: upload_id,  multipart_upload: {    parts: completed_parts  }})</code></pre><h2>Aborting and Cancelling</h2><p>At any point during the multipart upload process, you can abort or cancel theupload, which deletes any uploaded parts associated with the upload ID.</p><pre><code>s3.abort_multipart_upload({  bucket: bucket_name,  key: object_key,  upload_id: upload_id})</code></pre><p>The uploaded file will finally be available at <code>s3://bucket_name/object_id</code></p><p>S3 Multipart Uploads offers us several advantages:</p><h3>Fault tolerance</h3><p>We can resume uploads from where they left off in case of network failures orinterruptions. Also, uploading large objects in smaller parts reduces thelikelihood of timeouts and connection failures, especially in high-latency orunreliable network environments.</p><h3>Upload speed optimization</h3><p>With multipart uploads, you can parallelize the process by uploading multipleparts concurrently, optimizing transfer speeds and reducing overall upload time.</p>]]></content>
    </entry><entry>
       <title><![CDATA[Building a web-based screen recorder]]></title>
       <author><name>Unnikrishnan KP</name></author>
      <link href="https://www.bigbinary.com/blog/build-web-based-screen-recorder-loom-alternative-part-1"/>
      <updated>2024-03-15T12:00:00+00:00</updated>
      <id>https://www.bigbinary.com/blog/build-web-based-screen-recorder-loom-alternative-part-1</id>
      <content type="html"><![CDATA[<p>This is part 1 of our blog on how we are building<a href="https://www.neeto.com/neetorecord">NeetoRecord</a>, a Loom alternative. Here are<a href="https://www.bigbinary.com/blog/persistant-storage-for-recordings-in-s3-loom-alternative-part-2">part 2</a>and<a href="https://www.bigbinary.com/blog/mp4_transmuxing_and_streaming_support-loom-alternative-part-3">part 3</a>.</p><p>At <a href="https://neeto.com">neeto</a>, the product team, developers, and the UI teamoften communicate using short videos and screen recordings. We relied on popularsolutions like Loom and Bubbles. But they allowed only a small number ofrecordings in their free versions, and soon, they presented us with the upgradedscreens - upgrades were quite expensive for our team due to our team size andthe number of recordings we made daily.</p><p>So, we decided to build our own solution. We found the browser'sMediaStream Recording API.</p><h2>MediaStream Recording API</h2><p>The MediaStream Recording API, sometimes called the MediaRecorder API, isclosely affiliated with the<a href="https://developer.mozilla.org/en-US/docs/Web/API/Media_Capture_and_Streams_API">Media Capture and Streams API</a>and the<a href="https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API">WebRTC API</a>. TheMediaStream Recording API enables capturing the data generated by a MediaStreamor HTMLMediaElement. Captured video data is in WebM format. We can play it backlater using the<a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement">HTMLVideoElement</a>on any video player that supports WebM playback.</p><p>We will build a basic recorder that records the screen, audio from themicrophone, and then plays it back. We will first look at different fragments ofcode for recording the screen, recording audio, playing back in the browser andthen downloading the video file. At the end, we will combine them into afully working web-based screen recorder program.</p><h3>Record the screen</h3><pre><code class="language-javascript">let mediaRecorder;let recordedChunks = [];const stream = await navigator.mediaDevices.getDisplayMedia({  video: true,});mediaRecorder = new MediaRecorder(stream);mediaRecorder.ondataavailable = event =&gt; {  recordedChunks.push(event.data);};</code></pre><p><code>getDisplayMedia()</code> is provided by the<a href="https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API">WebRTC</a> (WebReal-Time Communication) API. It captures the contents of the user's screen orspecific application windows. <code>getDisplayMedia()</code> method prompts the user toselect and grant permission to capture the contents of a display or portionthereof (such as a window) as a MediaStream.</p><p>There is a similar method called <code>getUserMedia()</code>. It is typically used forapplications like video conferencing and live streaming. When you call<code>getUserMedia()</code>, the browser prompts the user for permission to access theircamera and microphone.</p><p>When there is recorded data available, <code>ondataavailable()</code> callback istriggered. We could process the data in this callback. In our case, we collectthe data by appending it to an array named <code>recordedChunks</code>.</p><h3>Record audio</h3><pre><code class="language-javascript">let audioStream = await window.navigator.mediaDevices.getUserMedia({  audio: { echoCancellation: true, noiseSuppression: true },});</code></pre><p>To capture audio, we use <code>getUserMedia()</code>. It is typically used for applicationslike video conferencing and live streaming. When you call <code>getUserMedia()</code>, thebrowser prompts the user for permission to access their camera and microphone.But we want to capture only the audio, so we pass the <code>audio</code> parameter.</p><p>The <code>audio</code> key accepts a set of parameters that would let us control thequality and properties of the captured audio stream. In our example, we haveenabled <code>echoCancellation</code> and <code>noiseSuppression</code> - two good features that wouldenhance the quality of our screen recordings. The complete list of audio optionsis available<a href="%5Bhttps://developer.mozilla.org/en-US/docs/Web/API/MediaTrackSettings#instance_properties_of_audio_tracks">here</a>.</p><p>The audio stream could be composed of multiple audio tracks - the microphone,system sounds, etc. We will add these tracks to the video stream we hadpreviously set up using <code>getDisplayMedia()</code>.</p><pre><code class="language-javascript">audioStream.getAudioTracks().forEach(audioTrack =&gt; stream.addTrack(audioTrack));</code></pre><h3>Playback the recording</h3><p>We now have an array named <code>recordedChunks</code>, which contains sequential chunks ofthe recorded data. Video players need video data as a<a href="https://developer.mozilla.org/en-US/docs/Web/API/Blob">Blob</a>. A blob is afile-like object of immutable, raw binary/text data. We must convert our<code>recordedChunks</code> array into a <code>Blob</code> to be played back or written into a file.</p><p>To construct a Blob from other non-blob objects and data, we can use the<code>Blob()</code> constructor.</p><pre><code class="language-javascript">const blob = new Blob(recordedChunks, {  type: &quot;video/webm&quot;,});</code></pre><p>Suppose we have an<a href="https://www.w3schools.com/tags/tag_video.asp">HTML video tag</a> in our page.</p><pre><code class="language-html">&lt;video id=&quot;recordedVideo&quot; controls&gt;&lt;/video&gt;</code></pre><p>When the video recording is stopped, we could create an Object URL for ourrecording <code>blob</code> and attach it to the video player.</p><pre><code class="language-javascript">mediaRecorder.onstop = () =&gt; {  let recordedVideo = document.getElementById(&quot;recordedVideo&quot;);  recordedVideo.src = URL.createObjectURL(blob);};</code></pre><p>We can now play the recording on the HTML video player.</p><p>Similarly, we can create a download link using the Object URL for the recording<code>blob</code>.</p><pre><code class="language-javascript">let a = document.createElement(&quot;a&quot;);let url = URL.createObjectURL(blob);a.href = url;</code></pre><p>We can now download and play the recording locally on any video playersupporting WebM playback.</p><h2>Putting it all together</h2><p>We have glued together the code fragments discussed above and created a<a href="/blog/neeto_record/basic_screen_recorder.html">demo</a> for a basic web-basedscreen recorder.</p><p>You may view the source code<a href="https://gist.github.com/unnitallman/6a054300f8bba645d42fd04008ea6ff1">here</a>.</p><h2>Next steps</h2><p>Now that we have a basic screen recorder in place, we have to consider thefollowing:</p><ol><li>Persistent storage.</li><li>Chunked uploading.</li><li>CDN support.</li><li>Playback with support for streaming.</li></ol><p>We will cover these topics in the next set of blogs. Stay tuned.</p>]]></content>
    </entry>
     </feed>