<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Northbeam Technologies</title>
    <description>The latest articles on DEV Community by Northbeam Technologies (@northbeamtech).</description>
    <link>https://dev.to/northbeamtech</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4000037%2F6e8470cf-e896-4f4f-af7e-ec767840cfae.jpg</url>
      <title>DEV Community: Northbeam Technologies</title>
      <link>https://dev.to/northbeamtech</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/northbeamtech"/>
    <language>en</language>
    <item>
      <title>Automating Student Onboarding via WhatsApp in SugarCRM</title>
      <dc:creator>Northbeam Technologies</dc:creator>
      <pubDate>Fri, 26 Jun 2026 07:41:34 +0000</pubDate>
      <link>https://dev.to/northbeamtech/automating-student-onboarding-via-whatsapp-in-sugarcrm-pad</link>
      <guid>https://dev.to/northbeamtech/automating-student-onboarding-via-whatsapp-in-sugarcrm-pad</guid>
      <description>&lt;p&gt;A multi-step conversation engine that captures leads and creates CRM records automatically — no staff required.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;When prospective students first contact the school via WhatsApp, staff had to manually collect their details and create a record in SugarCRM by hand. Leads that arrived outside business hours were simply missed or delayed — there was no way to capture them without a person in the loop.&lt;/p&gt;

&lt;p&gt;The school was already using Twilio for outbound WhatsApp messages from SugarCRM, but replies from unknown numbers went nowhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Built
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;multi-step conversation engine&lt;/strong&gt; built directly into SugarCRM. When an unknown number sends a WhatsApp message, the system:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Intercepts the message via a Twilio webhook&lt;/li&gt;
&lt;li&gt;Checks whether the phone number already exists as a student record&lt;/li&gt;
&lt;li&gt;If yes → routes to the existing follow-up flow (no change)&lt;/li&gt;
&lt;li&gt;If no → starts a guided intake questionnaire, validates each answer, and creates the student record automatically&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Zero admin involvement for new contacts.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F438sbz766t5jvjxxpy3o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F438sbz766t5jvjxxpy3o.png" alt=" " width="760" height="1120"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Technical Decisions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;State machine, not a script.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Each conversation is a session with a &lt;code&gt;current_step&lt;/code&gt; pointer. Questions can be reordered, toggled, or mapped to any CRM field without changing code. Admins manage the question bank directly from SugarCRM's UI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Phone number normalization.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
The system normalizes Israeli, US, and Vietnamese number formats before matching — preventing duplicate records from format variations like &lt;code&gt;+1-555...&lt;/code&gt; vs &lt;code&gt;001555...&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Media guard.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
If a student sends a photo or voice note, the system replies politely that only text is supported and automatically resends the current question.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Partial completion handling.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
If a student stops mid-flow, automated reminders go out at 2h and 24h. If minimum data (name + phone) is collected and the student still doesn't respond, a partial record is saved so the lead isn't lost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-number support.&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Multiple Twilio sender numbers can be registered in SugarCRM. Each conversation session locks to the number the student originally contacted.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Changed After Launch
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;✅ Student record creation from WhatsApp is fully automated for new contacts&lt;/li&gt;
&lt;li&gt;✅ Leads outside business hours are captured without any staff intervention&lt;/li&gt;
&lt;li&gt;✅ Admins can view full chat history, manually resume sessions, or override any step from inside SugarCRM&lt;/li&gt;
&lt;li&gt;✅ The existing outbound messaging workflow for known students is completely unchanged&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PHP&lt;/strong&gt; — core logic and SugarCRM custom modules&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SugarCRM&lt;/strong&gt; — custom modules for session management, question bank, and student records&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Twilio WhatsApp API&lt;/strong&gt; — inbound webhook + outbound messaging&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MySQL&lt;/strong&gt; — session state persistence&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;The main lesson here: WhatsApp bots don't need a separate platform. If your CRM already has a webhook endpoint and an API, you can build the conversation engine right inside it — and keep everything in one place for your team.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://northbeam-tech.com/whatsapp-student-onboarding-sugarcrm/" rel="noopener noreferrer"&gt;northbeam-tech.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>php</category>
      <category>crm</category>
      <category>twilio</category>
      <category>automation</category>
    </item>
    <item>
      <title>Client-Side Video Transcoding with FFmpeg.wasm — No Server Required</title>
      <dc:creator>Northbeam Technologies</dc:creator>
      <pubDate>Wed, 24 Jun 2026 08:02:20 +0000</pubDate>
      <link>https://dev.to/northbeamtech/client-side-video-transcoding-with-ffmpegwasm-no-server-required-4lj9</link>
      <guid>https://dev.to/northbeamtech/client-side-video-transcoding-with-ffmpegwasm-no-server-required-4lj9</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://publish.kolel.org/" rel="noopener noreferrer"&gt;Kolel Creator Dashboard&lt;/a&gt; lets educators and rabbis upload video lessons directly from their browser. Creators record on phones, tablets, and cameras — producing files in every format imaginable: &lt;code&gt;.mov&lt;/code&gt;, &lt;code&gt;.webm&lt;/code&gt;, &lt;code&gt;.avi&lt;/code&gt;, &lt;code&gt;.mkv&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Our Google Cloud Video Transcoder pipeline expects &lt;strong&gt;MP4 input&lt;/strong&gt;. The original solution was a dedicated pre-upload server that re-encoded every file before it hit cloud storage. One more service to deploy, monitor, and pay for.&lt;/p&gt;

&lt;p&gt;We needed a way to normalize the format &lt;strong&gt;before the file ever left the creator's machine&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The solution: FFmpeg running inside the browser
&lt;/h2&gt;

&lt;p&gt;We integrated &lt;code&gt;@ffmpeg/ffmpeg&lt;/code&gt; into the Vue 2 upload component. When a creator picks a non-MP4 file, the browser:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Downloads the FFmpeg WASM core (~30 MB, cached after first load)&lt;/li&gt;
&lt;li&gt;Transcodes the file to MP4 inside a Web Worker&lt;/li&gt;
&lt;li&gt;Uploads the resulting blob directly to a GCS Signed URL&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The Rails API and the downstream transcoding pipeline never see anything other than a clean MP4.&lt;/p&gt;




&lt;h2&gt;
  
  
  The core transcoding function
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createFFmpeg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fetchFile&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@ffmpeg/ffmpeg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;convertToMp4&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;supportedTypes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;video/mp4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;video/mov&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;video/m4v&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;supportedTypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// already compatible — skip transcoding&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ffmpeg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createFFmpeg&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;log&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;corePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://unpkg.com/@ffmpeg/core@0.10.0/dist/ffmpeg-core.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isLoaded&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fileName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;outputFileName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;output.mp4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;writeFile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fileName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-i&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fileName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;outputFileName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;readFile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;outputFileName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;selectedFile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;video/mp4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things worth noting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;We pin a specific CDN version&lt;/strong&gt; of &lt;code&gt;@ffmpeg/core&lt;/code&gt; so the WASM binary is cached in the browser after the first upload session. Repeat visitors pay zero load cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;We bail early&lt;/strong&gt; for common formats (&lt;code&gt;mp4&lt;/code&gt;, &lt;code&gt;mov&lt;/code&gt;, &lt;code&gt;m4v&lt;/code&gt;). Running FFmpeg.wasm on a file that's already compatible wastes time and memory.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Uploading without exposing credentials
&lt;/h2&gt;

&lt;p&gt;The browser never holds GCS credentials. The Rails API generates a short-lived Signed URL for the specific file path, then the browser PUTs directly to GCS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;uploadFileToSignedUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signedUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;selectedFile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signedUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;selectedFile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;selectedFile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;prepareVideoUpload&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;loading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getSignedUrl&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uploadFileToSignedUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signedUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;selectedFile&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pickedThumbnail&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getImageSignedUrl&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uploadFileToSignedUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;thumbnailSignedUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pickedThumbnail&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addVideo&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// notify Rails API to create the video record&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pre-upload transcoding microservice&lt;/td&gt;
&lt;td&gt;Removed entirely&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Format errors reaching the pipeline&lt;/td&gt;
&lt;td&gt;Zero&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GCS credentials in the browser&lt;/td&gt;
&lt;td&gt;Never&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Upload experience for MP4/MOV users&lt;/td&gt;
&lt;td&gt;Unchanged&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FFmpeg.wasm is production-ready&lt;/strong&gt; for this use case. The WASM core is large, but browser caching makes repeat sessions fast.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signed URLs are the right pattern&lt;/strong&gt; for direct browser-to-cloud uploads. No credentials ever leave your server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always allow-list known-good formats&lt;/strong&gt; before invoking WASM — it saves real time for the majority of your users.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Stack
&lt;/h2&gt;

&lt;p&gt;Vue 2 · @ffmpeg/ffmpeg 0.10 · @ffmpeg/core (WebAssembly) · Google Cloud Storage · GCS Signed URLs · Google Cloud Video Transcoder · Rails 7 · Axios&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://northbeam-tech.com/kolel-ffmpeg-wasm-browser-video-transcoding/" rel="noopener noreferrer"&gt;Northbeam Technologies blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webassembly</category>
      <category>javascript</category>
      <category>vue</category>
      <category>ffmpeg</category>
    </item>
  </channel>
</rss>
