Skip to content Skip to sidebar Skip to footer

Direct Uploads Error Storing Video.mov. Status: 0 Active Storage

Building a video converter with Rails 6 and FFmpeg

Today's project pulls together a number of built-in Rails tools, plus a couple of incredibly useful open source projects to build a web application that can convert user-uploaded video files to MP4.

To demonstrate some of the modern parts of the standard Rail stack, we're going to go further than just building a converter. Nosotros will enhance the user feel during the upload and conversion process with Active Storage to handle directly file uploads, Activity Cable for real-fourth dimension updates on the video conversion, and Stimulus to dynamically update the DOM without needing a heavy Javascript framework. When we're finished, we'll take an ugly but fully functional web application that allows a user to upload a video file. After the file is uploaded, we'll convert the video to MP4 if needed, and so display that converted video to the user. It will look something like this:

A screen recording of a user clicking a submit button, seeing a progress bar, and then progress text that counts up as a video converts. When finished, the text is replaced with the video element

This guide assumes that you are comfy with Ruby on Rails and familiar with Agile Storage and Stimulus, merely y'all do non need to exist an adept on whatsoever of these tools to get value from this guide. We'll commencement with a brand new Rails 6 projection, however, you lot can follow along with an existing Rails 6 projection if you prefer.

If you use an existing projection you lot may need to complete additional setup steps for Stimulus, Activity Cable, and Active Storage that won't be covered in this guide.

Project setup

To outset, let'southward create our Rail application using webpack and Stimulus, install Active Storage, add a User scaffold to work from, and drift our database.

All standard Rails stuff here, hopefully zilch totally new yet!

                              track new upload_progress                --webpack                =stimulus                --skip-java                --database                =postgresql                -T                cd                upload_progress     rails db:create     runway g scaffold User name:string     rails active_storage:install     rails db:migrate                          

At this point, you can boot your track server with rail s and visit localhost:3000/users to run across the scaffold working equally expected. Create a few users if you want. There are no rules here.

Now that the projection is setup, permit's go into the fun stuff.

Uploading files with Active Storage

Our user will have a profile video, which nosotros'll utilize equally the base of operations for exploring video conversion afterward on in this guide.

To add a contour video, update the User model to add a profile_video backed by Active Storage.

                              # app/models/user.rb                class                User                <                ApplicationRecord                has_one_attached                :profile_video                stop                          

Thanks to the power of Active Storage we can at present merely add together the profile video to our controller and views and commencement uploading files right abroad.

Head to the users grade and add together a new course field for the video:

                              <!-- app/views/users/_form.html.erb -->                <                %=                form_with                (                model:                user                )                do                |                form                |                %                >                <                %                if                user.errors.any                ?                %                >                <div                id=                "error_explanation"                >                <h2><                %=                pluralize                (                user.errors.count                ,                "                mistake                ")                %                >                prohibited this user from being saved:</h2>                <ul>                <                %                user.errors.each                exercise                |                error                |                %                >                <li><                %=                error.full_message                %                ></li>                <                %                stop                %                >                </ul>                </div>                <                %                end                %                >                <div                class=                "field"                >                <                %=                class.label                :name                %                >                <                %=                form.text_field                :proper noun                %                >                </div>                <div                class=                "field"                >                <                %=                class.label                :profile_video                %                >                <                %=                form.file_field                :profile_video                %                >                </div>                <div                class=                "deportment"                >                <                %=                grade.submit                %                >                </div>                <                %                end                %                >                          

Then update the UsersController to add together profile_video to the existing user_params method.

                              # app/controllers/users_controller.rb                def                user_params                params                .                require                (                :user                ).                let                (                :name                ,                :profile_video                )                end                          

Finally, nosotros desire to be able to see the video after nosotros've uploaded information technology. Head to app/views/users/evidence and add a <video> chemical element to the folio.

                              <!-- app/views/users/show.html.erb -->                <video                controls                >                <source                src=                "<%= url_for(@user.profile_video) %>"                >                </video>                          

Try information technology out and make sure that uploading videos works as expected by creating a new user and attaching a video file to the profile_video field.

A screenshot showing a video displayed on a plain white page

Stunning. You're a star.

While this works, we can make the upload experience a little improve.

Kickoff - when the file upload dialog opens, the user tin choose any type of file they like. Let's add a guardrail to just allow video files to be selected in the upload dialog.

                              <!-- app/views/users/_form_.html.erb -->                <                %=                grade.file_field                :profile_video                ,                accept:                "                video                /                *                "                %                >                          

Note that in a real app accept is a squeamish user feel gain but it does not validate the user'due south input. You should ever validate the file type on the server! A malicious or curious user tin can easily change the accept attribute in the browser. For today nosotros tin can get by with field-level validation on the customer.

Adding directly uploads and an upload progress bar

When a user uploads a big file direct to your servers, they are going to discover the asking takes a long fourth dimension and, in product, it may ofttimes fourth dimension out, block other requests, and cause all kinds of headaches.

Nosotros can prevent all of those issues with the straight upload feature congenital in to Active Storage. Direct upload sends files directly from the user'southward client to your chosen deject storage provider. Directly uploads speed upward the upload and keeping the request from timing out or backing upward your servers.

Allow'southward add straight upload to the file field and, as a bonus, add an upload progress bar for the user.

First, confirm that you have ActiveStorage'due south javascript added to your awarding.js file.

                              // application.js                // If y'all started with a new Runway project, these lines should already exist in application.js. If not, add them                import                *                as                ActiveStorage                from                "                @rail/activestorage                "                ActiveStorage                .                showtime                ()                          

Now, we need to tell our file field to use the directly upload javascript:

                              <!-- app/views/users/_form_.html.erb -->                <                %=                form.file_field                :profile_video                ,                have:                "                video                /                *                ",                direct_upload:                truthful                %                >                          

Refresh your page and confirm that everything works exactly as it did before we added straight upload. Again, Runway makes things actually easy.

While our file upload is working and we could exit it every bit it is, in the real earth uploads can take a while, specially when you're handling large video files. Giving the user feedback as their file uploads is a nice UX win. Allow'south add a progress bar to track the file upload using a few sprinkles of Stimulus.

To track upload progress, nosotros'll mind for the direct-upload:progress result and, each time that event is emitted we'll employ the information in the effect to update a progress bar in the UI.

We'll start with our Stimulus controller, which looks like this:

                              // app/javascript/controllers/upload_progress_controller.js                import                {                Controller                }                from                '                stimulus                '                export                default                course                extends                Controller                {                static                targets                =                [                "                progress                "                ,                "                progressText                "                ,                "                progressWidth                "                ]                initialize                ()                {                }                connect                ()                {                this                .                element                .                addEventListener                (                "                direct-upload:progress                "                ,                this                .                updateProgress                .                bind                (                this                ))                this                .                chemical element                .                addEventListener                (                "                direct-upload:error                "                ,                consequence                =>                {                effect                .                preventDefault                ()                const                {                id                ,                error                }                =                upshot                .                detail                console                .                log                (                error                )                })                }                showProgress                ()                {                this                .                progressTarget                .                style                .                display                =                "                block                "                }                updateProgress                ()                {                const                {                id                ,                progress                }                =                event                .                detail                this                .                progressWidthTarget                .                style                .                width                =                `                ${                Math                .                round                (                progress                )}                %`                this                .                progressTextTarget                .                innerHTML                =                `                ${                Math                .                round                (                progress                )}                % complete`                }                disconnect                ()                {                this                .                element                .                removeEventListener                (                "                direct-upload:progress                "                ,                this                .                updateProgress                )                }                }                          

Allow'south suspension this downwardly a footling fleck - I'm bold you've got a piffling bit of familiarity with Stimulus here, if you don't, the Stimulus Handbook is a great starting point.

connect is used to setup our event listener and subscribe to the events we care virtually - in this case, direct-upload:progress and directly-upload:fault

Our progress event listener calls updateProgress and then uses the progress data from the upshot to update the UI elements that make upward our progress bar.

Next nosotros'll add together our progress bar HTML and connect our Stimulus controller to the DOM.

                              <!-- app/views/users/_form_.html.erb -->                <                %=                form_with                (                model:                user                ,                html:                {                data:                {                controller:                "                upload-progress                "                }                }                )                practice                |                form                |                %                >                <!-- snipped class fields -->                <div                manner=                "display: none;"                information-upload-progress-target=                "progress"                >                <div>Uploading your video to our servers:                <span                id=                "progressText"                data-upload-progress-target=                "progressText"                >Warming upward...</bridge></div>                <div                id=                "progress-bar"                mode=                "background: gray; position: relative; width: 200px; pinnacle: 20px;"                >                <div                id=                "progressWidth"                data-upload-progress-target=                "progressWidth"                style=                "width: 0%; pinnacle: 100%; position: absolute; background: light-green;"                >                </div>                </div>                </div>                <div                class=                "deportment"                >                <                %=                form.submit                "                Salvage                ",                data:                {                action:                "                click-                >upload-progress#showProgress" } %>                </div>                <                %                end                %                >                          

There's a lot here, let'south pace through the updates individually.

First, nosotros connect our Stimulus controller to the DOM past adding data: { controller: "upload-progress" } to our form_with

Then, we add our progress bar HTML. Don't mind all the inline styles, yous can use CSS if you like.

                              <div                mode=                "display: none;"                information-upload-progress-target=                "progress"                >                <div>Uploading your video to our servers:                <bridge                id=                "progressText"                information-upload-progress-target=                "progressText"                >Warming upwards...</span></div>                <div                id=                "progress-bar"                style=                "background: gray; position: relative; width: 200px; meridian: 20px;"                >                <div                id=                "progressWidth"                information-upload-progress-target=                "progressWidth"                style=                "width: 0%; height: 100%; position: absolute; background: green;"                >                </div>                </div>                </div>                          

The progress bar is hidden when the page loads and we use Stimulus targets on our progress bar container, progress text, and progress width elements. The Stimulus controller uses these targets to update the DOM every bit directly-upload:progress events are emitted.

Finally, on the course'south submit push, nosotros listen for a click. When the grade submit button is clicked, we call upload-progress#showProgress to remove the hidden style from the progress bar container.

Adding the video converter service

Now we've got our profile videos uploading nicely and nosotros are informing users of the progress throughout.

Smashing progress, but nosotros are accepting and displaying any video blazon and our goal is to build a video converter so that we have a predictable video type on profile videos. For this tutorial, let's say that our requirements are that all uploaded videos must end up as MP4s.

Before we dive in to the details, let's review what nosotros want to accomplish at a loftier level. We want to:

  1. Permit a user to upload a video file through the User Profile form
  2. When a video file is uploaded, check the video's content type
  3. If the video is already an mp4 file, nosotros don't need to convert the video - our work is done
  4. If the video is not an mp4 file, we want to transcode the video from its original type to .mp4

Then, how do we accomplish footstep four? Converting a video on the fly sounds complicated, right?

Enter FFmpeg, an incredibly popular, open up source solution for working with video (and audio, if that'south your affair).

While nosotros can work with FFmpeg directly, we're going to utilise a gem to make our interaction with FFmpeg a little simpler: streamio-ffmpeg

Allow's start by calculation the gem to our project with parcel add streamio-ffmpeg and installing ffmpeg on our system with brew install ffmpeg on a Mac. Other installation options can be institute here

With FFmpeg ready, let's add together a service to handle video conversion with mkdir app/services && affect app/services/video_converter.rb

Here'southward what the service looks similar:

                              # app/services/video_converter.rb                class                VideoConverter                def                initialize                (                user_id                )                @user                =                User                .                find                (                user_id                )                stop                def                convert!                process_video                cease                private                def                process_video                @user                .                profile_video                .                open up                (                tmpdir:                                "/tmp"                )                do                |                file                |                movie                =                FFMPEG                ::                Film                .                new                (                file                .                path                )                path                =                "tmp/video-                #{                SecureRandom                .                alphanumeric                (                12                )                }                .mp4"                movie                .                transcode                (                path                ,                {                video_codec:                                'libx264'                ,                audio_codec:                                'aac'                })                @user                .                profile_video                .                adhere                (                io:                                File                .                open                (                path                ),                filename:                                "video-                #{                SecureRandom                .                alphanumeric                (                12                )                }                .mp4"                ,                content_type:                                'video/mp4'                )                end                end                end                          

The important role of the service is the process_video method, so permit'south zoom in there.

Starting time, we open the existing profile video attached to the user, then we can access the video's path.

@user.profile_video.open up(tmpdir: "/tmp") practice |file|

Next, we create a Motion picture object with the streamio-ffmpeg gem, using the original file uploaded by the user, which we're going to transcode soon.

movie = FFMPEG::Motion picture.new(file.path)

The path variable assignment is the location where nosotros will create the new, transcoded video. Using this new path and the Picture show object, nosotros call the transcode method from the streamio-ffmpeg precious stone.

Finally, nosotros adhere the newly created video to the user, replacing the video that was previously uploaded. Easy!

@user.profile_video.attach(io: File.open(path), filename: "video-#{SecureRandom.alphanumeric(12)}.mp4", content_type: 'video/mp4')

Adding a background task

One more than step before we can showtime converting uploaded videos. Working with video tin be time and resource intensive. We don't want to convert a video during a folio turn or block our application servers with expensive video processing.

Let's add a job that we tin use to enqueue video processing jobs in the background. For this tutorial, we'll apply ActiveJob with the default :async adapter, just in production you'll desire to use a real background processor.

Add the job with rails g job convert_video

Our chore receives a user id and calls the video converter service for that user, like this:

                              class                ConvertVideoJob                <                ApplicationJob                queue_as                :default                def                perform                (                user_id                )                VideoConverter                .                new                (                user_id                ).                convert!                cease                end                          

Converting uploaded videos

Now we're ready to convert videos. In our UsersController, nosotros'll enqueue the background job in our create and update methods, after the user is saved.

                              # app/controllers/users_controller.rb                def                create                @user                =                User                .                new                (                user_params                )                respond_to                do                |                format                |                if                @user                .                save                ConvertVideoJob                .                perform_later                (                @user                .                id                )                # snip response boilerplate                terminate                end                end                def                update                respond_to                do                |                format                |                if                @user                .                update                (                user_params                )                ConvertVideoJob                .                perform_later                (                @user                .                id                )                # snip response boilerplate                finish                end                end                          

Now when nosotros upload a profile video, our VideoConverter service volition convert the video to mp4 and supercede the uploaded video with the newly converted video.

Try it out by uploading whatsoever non-mp4 video from the users form. In your server logs y'all should see output that looks like this if everything is working every bit expected:

INFO -- : Transcoding of /tmp/ActiveStorage-22-20210428-10611-1wvew4g.mov to tmp/video-9ZrvVFnAZZTJ.mp4 succeeded

But expect a minute. Transcoding can accept a while and information technology happens in the background, and so when I upload a video, the show page will render the non-transcoded video until after transcoding is complete. That's going to cause all kinds of weird bugs, correct?

Right.

Permit'due south get fancy with Action Cablevision, plus a few more sprinkles of Stimulus to track video conversion progress and return the converted video without a folio plough.

Using Activity Cablevision to broadcast video updates

First let's talk through the trouble again at a loftier level. We desire to reach something like this:

  1. When a video is uploaded, bank check to see if it is an mp4
  2. If it isn't, flag the video as needs conversion
  3. If the video is flagged as needs conversion, don't render the video chemical element on the user's evidence page
  4. If a video is being processed, communicate that to the user
  5. If the video has been processed, or did not need to be candy, render the video element on the show page

Let'southward start with the logic nosotros skipped in the terminal section to check if the video needs to be transcoded. Since there'due south no indicate in converting an mp4 to an mp4, we should check the uploaded video'due south content blazon before doing whatever processing.

If the content type is video/mp4, then we don't need to transcode the video. If the content type is annihilation else, flag the video and get-go converting.

Before we write the logic code, let'southward add together a boolean value to our User model to track whether the profile video needs to exist converted:

track g migration AddConvertVideoToUsers convert_video:boolean

Drift your database before moving on rails db:migrate

Now we tin can add the logic to our controller. In the real world, the controller probably isn't the right place for this code, simply we're here to learn, then let's stay focused.

                              # app/controllers/users_controller.rb                def                create                @user                =                User                .                new                (                user_params                )                respond_to                do                |                format                |                if                @user                .                save                update_conversion_value                ConvertVideoJob                .                perform_later                (                @user                .                id                )                # snip render logic                finish                end                end                # PATCH/PUT /users/one or /users/1.json                def                update                respond_to                practise                |                format                |                if                @user                .                update                (                user_params                )                update_conversion_value                ConvertVideoJob                .                perform_later                (                @user                .                id                )                # snip render logic                end                cease                end                private                # snip                def                update_conversion_value                return                unless                @user                .                profile_video                needs_conversion                =                @user                .                profile_video                .                content_type                !=                "video/mp4"                @user                .                update_column                (                :convert_video                ,                needs_conversion                )                end                          

The slightly clunky update_conversion_value method checks the content blazon of the profile video and assigns the appropriate value to the convert_video column.

Now that we've got this logic in place, nosotros tin head into our converter service and put that logic to use.

                              # app/services/video_converter.rb                def                convert!                return                unless                @user                .                convert_video?                process_video                cease                          

This modify ensures that our video converter doesn't catechumen the video unless it has been flagged for conversion.

Adjacent we want to update the value of convert_video afterwards we've converted non-mp4 videos. Nosotros tin do that with a new update_needs_conversion method in our service:

                              # app/services/video_converter.rb                grade                VideoConverter                def                convert!                return                unless                @user                .                convert_video?                process_video                update_needs_conversion                terminate                private                # Snip                def                update_needs_conversion                @user                .                update_column                (                :convert_video                ,                faux                )                end                end                          

Now when nosotros upload a video, the converter will return without doing annihilation if the video is an mp4, otherwise information technology will convert the video then update the convert_video flag on the user to imitation.

With this flag in place, we can add in our Action Cable and Stimulus magic to communicate a video's conversion status to the user so they can meet their uploaded video without refreshing the page.

Let'south think our goal for this part of the project. Nosotros want to:

  1. Check the video'south conversion condition when nosotros render the user testify page
  2. If the video is converted, display the video in a video chemical element
  3. If the video is not converted, display a placeholder for the video and communicate the video's conversion condition to the user
  4. When the video finishes converting, automatically update the content of the user's prove page to display the video in a video element

Let'due south dig in.

Outset, nosotros need to generate an Action Cable channel to broadcast from. As usual, Rails comes with a congenital in converter for this. Use track thou channel VideoConversion to generate the new aqueduct

Update the generated video_conversion_channel.rb file to expect similar this:

                              class                VideoConversionChannel                <                ApplicationCable                ::                Aqueduct                def                subscribed                stream_from                "video_conversion_                #{                params                [                :id                ]                }                "                end                terminate                          

This channel is responsible for broadcasting the progress of the video conversion process, updating a percentage consummate element in the UI.

Next, allow's add the Stimulus controller that will listen for events from this aqueduct and update the UI as it receives them.

First touch on app/javascript/controllers/conversion_progress_controller.js

And then:

                              // app/javascript/controllers/conversion_progress_controller.js                import                {                Controller                }                from                "                stimulus                "                ;                import                consumer                from                "                channels/consumer                "                ;                consign                default                class                extends                Controller                {                static                targets                =                [                "                progressText                "                ]                initialize                ()                {                this                .                subscription                =                consumer                .                subscriptions                .                create                (                {                channel                :                "                VideoConversionChannel                "                ,                id                :                this                .                element                .                dataset                .                id                ,                },                {                connected                :                this                .                _connected                .                demark                (                this                ),                asunder                :                this                .                _disconnected                .                bind                (                this                ),                received                :                this                .                _received                .                demark                (                this                ),                }                );                }                _connected                ()                {}                _disconnected                ()                {}                _received                (                information                )                {                this                .                updateProgress                (                information                *                100                )                }                updateProgress                =                (                progress                )                =>                {                allow                progressPercent                =                ''                if                (                progress                >=                100                )                {                progressPercent                =                "                100%                "                }                else                {                progressPercent                =                Math                .                round                (                progress                )                +                "                %                "                }                this                .                progressTextTarget                .                innerHTML                =                progressPercent                }                }                          

This controller contains a lot of Action Cable average, don't permit it overwhelm you.

The important parts are the channel we subscribe to in the initialize method and the _received method. When a new message is broadcast on the aqueduct that the user is subscribed to (from the id in the initialize method), _received calls updateProgresswhich updates the DOM with the progress value circulate by Activeness Cable.

Let's wire this controller up to our HTML and start to bring it all together.

We desire to subscribe to updates on a detail user, which ways we demand to update our show view to connect to the ConversionProgress Stimulus controller.

                              <!-- views/users/show.html.erb -->                <!-- snip -->                <div                data-id=                "<%= @user.id %>"                data-controller=                "conversion-progress"                fashion=                "max-width: 500px; max-height: 500px;"                >                <                %                if                @                user.convert_video                ?                %                >                <div>                <p>Nosotros are converting your video. The video is currently                <bridge                data-conversion-progress-target=                "progressText"                >0%</span>                processed</p>                </div>                <                %                else                %                >                <video                controls                style=                "max-width: 100%; max-peak: 100%;"                >                <source                src=                "<%= url_for(@user.profile_video) %>"                >                </video>                <                %                end                %                >                </div>                <!-- snip -->                          

Here we've added a information-id and a data-controller to the video's parent video. data-id is used by the Stimulus controller to know which aqueduct to subscribe to updates from, and the data-controller is used to connect the Stimulus controller to the DOM.

The other change here is adding logic to display the video as-is when the video has been converted, otherwise, nosotros render text that our Stimulus controller will update equally it receives updates from Action Cablevision.

After making these changes, if you upload a new non-mp4 video to a user and visit the evidence page, y'all'll see the "Nosotros are converting your video" text but the percentage candy will never update. That is because we aren't yet broadcasting the conversion progress to the Activeness Cable channel.

A screenshot of plain text displaying 0% progress converting the video

Fortunately, the FFmpeg jewel makes dissemination progress actually uncomplicated. Let's update the transcode call in our video converter service to broadcast the modify:

                              movie                .                transcode                (                path                ,                {                video_codec:                                'libx264'                ,                audio_codec:                                'aac'                })                {                |                progress                |                ActionCable                .                server                .                broadcast                (                "video_conversion_                #{                @user                .                id                }                "                ,                progress                )                }                          

At present nosotros'll see our progress text count up from 0% to 100% when we upload a new video that needs conversion. Depending on the video's properties and your computer'due south power, this might exist a very fast process or information technology could take several minutes. Either fashion, you tin now watch the progress in real fourth dimension!

When the percentage gets to 100% you'll notice one final upshot blocker that we demand to solve. Instead of replacing the video placeholder with the actual video when information technology reaches 100% conversion, the counter just stays there and the user has to refresh the page to see the video. Nosotros can prepare that with a lilliputian more than Action Cable, and a trivial more Stimulus.

First, add some other Action Cable channel with runway g channel ConvertedVideo and update the subscribed method in the generated _channel.rb file:

                              class                ConvertedVideoChannel                <                ApplicationCable                ::                Aqueduct                def                subscribed                stream_from                "converted_video_                #{                params                [                :id                ]                }                "                end                finish                          

Then add together a new Stimulus controller for subscribing to the channel and managing updates with impact app/javascript/controllers/converted_video_controller.js

And add the code to subscribe and handle broadcasts on the ConvertedVideo channel.

                              // javascript/controllers/converted_video_controller.js                import                {                Controller                }                from                "                stimulus                "                ;                import                consumer                from                "                channels/consumer                "                ;                consign                default                class                extends                Controller                {                static                targets                =                [                "                videoContainer                "                ];                initialize                ()                {                this                .                subscription                =                consumer                .                subscriptions                .                create                (                {                channel                :                "                ConvertedVideoChannel                "                ,                id                :                this                .                element                .                dataset                .                id                ,                },                {                connected                :                this                .                _connected                .                bind                (                this                ),                disconnected                :                this                .                _disconnected                .                bind                (                this                ),                received                :                this                .                _received                .                demark                (                this                ),                }                );                }                _connected                ()                {}                _disconnected                ()                {}                _received                (                data                )                {                const                videoElement                =                this                .                videoContainerTarget                videoElement                .                innerHTML                =                data                }                }                          

This Stimulus controller is very like to the final controller. In it, nosotros subscribe to the ConvertedVideoChannel with the user id. When information is broadcast on the channel, the Stimulus controller looks for a videoContainer DOM element and replaces the content of that element with the information sent from Action Cablevision. We'll run into what that information looks like next.

Our goal here is to replace the container of the video placeholder element with the actual video once information technology has been candy. Nosotros can exercise this with Activeness Cable past taking advantage of the fact that we tin return a view partial to a string and circulate that string from Activity Cable, making it like shooting fish in a barrel to replace DOM content with HTML broadcast in a bulletin.

To commencement, let'south add together a fractional that renders the video element, and add that to our the view. While we're in the show view, we'll also connect the new Stimulus controller to the DOM.

First run bear upon app/views/users/_profile_video.html.erb in your concluding and then:

                              <!-- views/users/_profile_video.html.erb -->                <video                controls                style=                "max-width: 100%; max-meridian: 100%;"                >                <source                src=                "<%= url_for(user.profile_video) %>"                >                </video>                          
                              <!-- views/users/show.html.erb -->                <div                information-id=                "<%= @user.id %>"                information-controller=                "conversion-progress converted-video"                data-converted-video-target=                "videoContainer"                mode=                "max-width: 500px; max-pinnacle: 500px;"                >                <                %                if                @                user.convert_video                ?                %                >                <div>                <p>Nosotros are converting your video. The video is currently                <span                data-conversion-progress-target=                "progressText"                >0%</span>                candy</p>                </div>                <                %                else                %                >                <                %=                return                "                profile_video                ",                user:                @                user                %                >                <                %                finish                %                >                </div>                          

At present we've moved the video element to a fractional, and updated our show view to render the fractional when the video does not demand to be converted.

Take note of the commencement line in a higher place. We've added the new converted-video Stimulus controller to the video container's information-controller aspect. This connects the controller to the DOM and ensures that visitors to the evidence page are subscribed to the ConvertedVideo channel. Nosotros've also added a data-converted-video-target attribute to the same <div>. This target is used by the Stimulus controller to replace the progress text with the video element.

The last stride is to update the VideoConverter service to circulate a message containing the profile_video partial on the ConvertedVideo channel later the video has been converted.

                              # app/services/video_converter.rb      def convert!       return unless @user.convert_video?        process_video       update_needs_conversion       render_processed_video     end    private      # Snip     def render_processed_video       fractional = ApplicationController.render(fractional: "users/profile_video", locals: { user: @user })       ActionCable.server.broadcast("converted_video_#{@user.id}", partial)     stop                          

Here we're calling a new render_processed_video method from our convert! method. This method renders a partial to a string and then broadcasts that cord as data, to be picked up and used by our Stimulus controller. Magic.

Permit's see information technology in activeness.

A screen recording of a user clicking a submit button, seeing a progress bar, and then progress text that counts up as a video converts. When finished, the text is replaced with the video element

Wrapping up

Thanks for making information technology through this guide! Y'all can find the total source code for this guide on Github.

To recap, today nosotros started with a fresh Rails half-dozen awarding. With the ability of Stimulus, Active Storage, Activeness Cable and, nearly importantly, FFmpeg, we congenital an app that can convert a user-uploaded video file to mp4 when needed. While the file is converting, we communicate the progress to the user and display the converted user to them without asking them to reload the page.

To make the code in this tutorial production-fix, besides cleaning up the code and styling things, yous should spend time validating file uploads from the user, add together a existent background job processor, and add error-handling and resilience to the video conversion service.

You might also add validation for file size to the videos, on both the client and server, and consider more than efficient methods for converting videos to mp4 with more powerful FFmpeg functionality.

Further resources and contact info

If you lot'd similar to see a product version of this projection, have a look at the demo for Vestimonials, the product I'm building when I'm not writing technical articles like this ane.

If yous take questions or feedback on this article, yous tin can notice me on Twitter

Thanks for reading!

quarlesyoubtleas.blogspot.com

Source: https://www.colby.so/posts/building-a-video-converter-with-rails-and-ffmpeg

ارسال یک نظر for "Direct Uploads Error Storing Video.mov. Status: 0 Active Storage"