In our poll maker we frequently get the feature request to allow users to create and host their own video contest. We already allowed users to create video contests, where the content is not uploaded, but just linked from youtube or vimeo. We understood the arguments of our users, to have a simpler process for the users, where they don’t have to leave our platform and the contest creators have better access to the uploaded video data, but we hesitated to tackle the complexity of handling video data.

Fortunately the people from bunny.net are doing a great job to help you with everything you need for hosting videos including transcoding, worldwide distribution, digital rights management for a really competitive price. Also if you allow your users to upload such large files as videos, some processes or threads of your web server might be busy while you still need resources to serve your normal web requests. Luckily you can directly upload the content to bunny.net and your application can be notified about the progress through a webhook.

So here is what this tutorial is going to achieve:

  1. Create a page, with a form to upload the video.
  2. Submitting the form, first announces a new video to our backend and from there prepares the necessary parameters for the client to upload the video directly to bunny.net.
  3. The client uploads the video.
  4. When bunny.net changes the progress of processing or transcoding the video, a webhook in our backend is called.

Create the frontend code

We plan to upload the video directly to Bunny, without occupying our servers. Bunny has a tus endpoint, for resumable uploads. We are using Uppy, which has a tus implementation.

<form id="videoUploader">
  <label>Video</label>
  <input type="file" id="attachment" accept="video/mp4" />

  <input type="submit" value="Upload" />
</form>
import { Uppy, Tus } from "vendor/uppy";

$(function () {
  /*
   Instead of submitting the form, we are announcing the new video to our backend.
   There we create an empty video object through the Bunny API,
   and return the relevant parameters to the frontend.
   Then we can start uploading from the client to the Bunny endpoint.
   */
  $(document).on(
    "click",
    "#videoUploader input[type=submit]",
    async function (e) {
      e.preventDefault();
      const file = $("#attachment")[0].files[0];
      const params = await createEmptyVideo(file);
      uploadToBunny(file, params);
    }
  );
});

const createEmptyVideo = async function (file) {
  const data = new FormData();
  data.set("filename", file.name);
  const response = await fetch("/videos", {
    headers: { Accept: "application/json" },
    body: data,
    method: "POST",
  });
  const responseJson = await response.json();
  return responseJson.params;
};

const uploadToBunny = function (file, params) {
  const uppy = new Uppy().use(Tus, {
    endpoint: "https://video.bunnycdn.com/tusupload",
    headers: {
      AuthorizationSignature: params.signature,
      AuthorizationExpire: params.expiration_time,
      VideoId: params.video_guid,
      LibraryId: params.library_id,
    },
  });

  uppy.addFile({
    name: file.name,
    type: file.type,
    size: file.size,
    data: file,
  });

  uppy.upload().then((result) => {
    if (result.failed.length === 0) {
      console.log("Upload complete");
    } else {
      console.log("Upload failed");
    }
  });
};

Create the backend code

Create an endpoint which only needs the video title as param to create an empty video in Bunny Streams. The video is then filled with data by an upload directly to Bunny from the client browser. We are doing this in rails, but you can easily use the code in sinatra or others.

class VideosController < ApplicationController
   VIDEO_LIBRARY = "<yourlibrary_id>"
   API_KEY = "<bunny_cdn_api_key>"

   def create
      render json: upload_params(params[:filename]).to_json
   end

   private

   def upload_params(title)
      expiration_time = 1.day.since
      video_guid = create_video_guid(title)
      str = "#{library_id}#{API_KEY}#{expiration_time}#{video_guid}"
      signature = Digest::SHA256.hexdigest(str)
      { signature:, video_guid:, library_id: VIDEO_LIBRARY, expiration_time: }
   end

  def create_video_guid(title)
      response = Faraday.post(
         "https://video.bunnycdn.com/library/#{VIDEO_LIBRARY}/videos",
         { title: }.to_json,
         { "AccessKey" => API_KEY,
         "Content-Type" => "application/json",
         "Accept" => "application/json" }
      )
      JSON.parse(response.body)["guid"]
  end
end

Get notified about the video progress by webhook

It is great, our servers have nothing to do with handling the video upload, but therefore we would need to be notified once the upload is complete. We could do this from our client code, but from there we can only notify the success or failure of the upload. Bunny Stream offer a better way to notify your backend with a webhook. With this you can get notified about the different states a video goes through, like processing, transcoding and act accordingly.

In your Bunny Stream library, you can configure the webhook in API -> Webhook.

class VideosController
   ...
   # POST /videos/webhook
   def webhook
      case params["Status"].to_i
      when 3
         p "Video #{params['VideoGuid']} was finished successfully"
      when 5
         p "Processing or transcoding of video #{params['VideoGuid']} failed"
      end
   end
   ...
end

You can see the full list of status codes here: Bunny Webhook Status codes