Introduction

A video upload feature is commonly implemented inside applications. Solutions are often simple, but not straightforward. Problems happen when you consider a video upload feature from mobile and face constraints such as connection to server timeout on upload and pure network bandwidth on streaming.

The first option we considered was to use an external service for video storage and handle other aspects of video upload on our back-end application with a custom implementation. The best-fit service, in this case, is Amazon S3. On the other hand, we also considered using a complete service that will host our videos but also handle upload, processing/converting, and streaming.

Initially, we tried the first option and ended up with a lot of problems regarding very long processing times, since our machines were not specialized in video conversion and were slow in streaming to mobile applications. This caused very strict limitations for video size, even when the mobile applications were using fast networks. We could have used some other Amazon service to resolve this problem, but we decided to try a complete solution instead and chose Vimeo.

Upload process with Vimeo

Vimeo supports post, streaming, and pull methods. We chose streaming because it provides the most flexibility for tailoring the upload process to your application needs.

The first thing you need to do after creating a Vimeo account is to go to developer.vimeo.com/apps, choose your application, and click ‘Request upload access’. This can take a couple of days to become operational, so be sure to do this well before your deadline. Once you have successfully gained upload access you can proceed with choosing your preferred upload method.

Vimeo API supports three upload methods: post, streaming, and pull (automatic).

We chose the streaming approach for both our mobile and web applications because it provides more options for tailoring the upload flow to suit your application needs.

The streaming approach goes through five stages:
1. Check quota.
2. Get ticket.
3. Upload video.
4. Check if the video is fully uploaded.
5. Delete upload ticket.

Before starting this process you need to generate a ‘personal access token’ by going to developer.vimeo.com/apps and choosing your application. Under the Authentication tab you’ll see a list of access tokens and options for generating a new one. Under Scopes you need to choose public, private, upload, and edit. The first three are straight forward; we’ll see why we need to edit down the road.

In our case we jumped straight to the second step, since we already use a business Vimeo account with no upload limit. If you use a free, plus, or pro account you should first check if your upload limit has been reached. We also skipped the step that involves checking if the video is fully uploaded since our upload flow didn’t need a feature of continuing upload for partially uploaded video.

Snippets for Android will be written using the Retrofit library, which we use in order to consume REST APIs. Snippets are written in Java and for parsing of JSON we will use the Jackson library.

Snippets for iOS will be written in Objective-C, using just the Foundation framework without any external libraries.

Example for iOS and Android application

Get a ticket

In this step we need to get an upload ticket from the Vimeo API. We will use that upload ticket to perform a single file upload to the Vimeo API. The upload ticket lets us keep our Vimeo account secure because we don’t compromise our application even if someone gets hold of our upload ticket. In this step we need to send the following request:

POST

  https://api.vimeo.com/me/videos?fields=upload_link_secure,complete_uri

Headers

  Content-Type: application/x-www-form-urlencoded
  Authorization: bearer [your_vimeo_token]

Body

  type=streaming

Response

  {
  upload_link_secure,
  complete_uri
  }

 
// Request
@Headers("Authorization: bearer [your_vimeo_token]")
@FormUrlEncoded
@POST("/me/videos?fields=upload_link_secure,complete_uri")
Call<VimeoTicket> generateUploadTicket(@Field("type") String type);

// Setup
Retrofit retrofit = new Retrofit.Builder()
        .baseUrl("https://api.vimeo.com")
        .addConverterFactory(JacksonConverterFactory.create())
        .build();

VimeoApi vimeoApi = retrofit.create(VimeoApi.class);

// Call
Call<VimeoTicket> call = vimeoApi.generateUploadTicket("streaming");
call.enqueue(new Callback<VimeoTicket>() {
    @Override
    public void onResponse(Call<VimeoTicket> call, Response<VimeoTicket> response) {
        if (response.isSuccessful()) {
            // Continue to video upload
        } else {
            // Something went wrong with generating a ticket
        }
    }

    @Override
    public void onFailure(Call call, Throwable t) {
        //Generating ticket failed
    }
});
NSString *const VimeoAuthorization = @"Bearer <your key here>";
NSString *const TemporaryPostUrl = @“https://api.vimeo.com/me/videos?type=streaming&redirect_url=&upgrade_to_1080=false&fields=upload_link_secure,complete_uri";

…

// Call this method somewhere from application and pass video data as NSData object
- (void)uploadVideoData:(NSData *)videoData {
NSMutableURLRequest *request=[NSMutableURLRequest requestWithURL:[NSURL URLWithString:TemporaryPostUrl]];
[request setHTTPMethod:@"POST"];
// This could be extract to separate method, since it’s common for all requests
[request setValue:VimeoAuthorization forHTTPHeaderField:@"Authorization"];

NSError *error;

NSData *returnData = [NSURLConnection sendSynchronousRequest:request returningResponse:nil error: &error];
NSDictionary * json = [NSJSONSerialization JSONObjectWithData:returnData options:kNilOptions error:&error];

    if (!error) {
// Process to next step and send response, as well as videoData
    } else {
// Handle failure
    }
}

}

Once we’ve got the upload ticket (upload_link_secure field from response) we can proceed to the next step. Parameter fields is used to limit responses to only attributes that we will need. We’ll talk about why this is important later on.

Upload video

The second step is uploading the video file. To do this we use the upload_link_secure field that we got inside the ticket and proceed to make a request to the Vimeo API. Inside the body of this request we need to send the file bytes of the video file we want to upload. The request we need to send is:

PUT

  [upload_link_secure]

Headers

  Authorization: bearer [your_vimeo_token]

  Content-Type: [mime_type]

  Content-Length: [file_lenght_in_bytes]

Body

  [file_bytes]

// Request
@Headers("Authorization: bearer [your_vimeo_token]")
@PUT
Call<ResponseBody> uploadVideo(@Url String url, @HeaderMap Map<String, String> headers, @Body RequestBody video);

// Call
Call<ResponseBody> call = vimeoApi.uploadVideo(vimeoTicket.getUploadLinkSecure(), headers, video);
call.enqueue(new Callback<ResponseBody>() {
    @Override
    public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
        // Continue with delete upload ticket
    }

    @Override
    public void onFailure(Call<ResponseBody> call, Throwable t) {
        // Uploading video failed
    }
});
NSString *const AcceptHeader = @"application/vnd.vimeo.*+json; version=3.2";

…

// uploadUrl is "upload_link_secure" field in resposne fetched in the previous step
// competeUrl is "complete_uri" field in response fetched in the previous step, will be pass to the enxt step if this step be successful
- (void)uploadVideoToUrl:(NSString *)uploadUrl completeUrl:(NSString *)completeUrl videoData:(NSData *)videoData {

    NSURLSessionConfiguration *configuration;
    configuration.HTTPMaximumConnectionsPerHost = 1;

    NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration
                                                          delegate:self
                                                     delegateQueue:[NSOperationQueue mainQueue]];

    NSURL *url = [NSURL URLWithString:uploadUrl];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    [request setHTTPMethod:@"PUT"];

    [request setValue:VimeoAuthorization forHTTPHeaderField:@"Authorization"];
    [request setValue:AcceptHeader forHTTPHeaderField:@"Accept"];

    NSError *error;

    NSString *videoLength = [NSString stringWithFormat:@"%lu",(unsigned long)videoData.length];

    NSDictionary *dict = @{@"Content-Length" : videoLength,
                           @"Content-Type" : @"video/mp4"};
    [request setHTTPBody: [NSJSONSerialization dataWithJSONObject:dict options:0 error:&error]];

    NSURLSessionUploadTask *taskUpload = [session uploadTaskWithRequest:request fromData:videoData completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {

        NSHTTPURLResponse *response = (NSHTTPURLResponse*) response;
        if (!error && response.statusCode == 200) {
            // Handle success, pass completeUrl
        } else {
            // Handle failure
            }
    }];

    [taskUpload resume];
}

If everything goes smoothly, this request will return the status code 200.

Delete upload ticket

In this last step we use the complete_uri that we got as a response from generating the upload ticket. As soon as we signal to the Vimeo API that our upload process is finished, it can generate a video ID and URL. Deleting our upload ticket triggers the Vimeo API to start processing our video. This can take some time. The parameters that determine the processing speed will be described later on.

DELETE 

  https://api.vimeo.com[complete_uri]

Headers

Authorization: bearer [your_vimeo_token]
// Request
@Headers("Authorization: bearer [your_vimeo_token]")
@DELETE
Call<Void> deleteUploadTicket(@Url String url);

// Call
Call<Void> call = vimeoApi.deleteUploadTicket("https://api.vimeo.com" + vimeoTicket.getCompleteUri());
call.enqueue(new Callback<Void>() {
    @Override
    public void onResponse(Call<Void> call, Response<Void> response) {
        if (response.isSuccessful()) {
            // We can now extract video URL or edit metadata
        } else {
            // Delete of upload ticket failed
        }
    }

    @Override
    public void onFailure(Call call, Throwable t) {
        // Delete of upload ticket failed
    }
});
-(void)callCompleteUrl:(NSString *)completeUrl {

    NSString *urlString =[NSString stringWithFormat:@"%@%@", VimeoApiBaseUrl, completeUrl];
    NSMutableURLRequest *request=[NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlString] cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:0];
    [request setHTTPMethod:@"DELETE"];
    [request setValue:VimeoAuthorization forHTTPHeaderField:@"Authorization"];

    [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
        NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response;
        if (!error) {
            // Handle success
        }
        else {
            // Handle failure
        }
    }];
}

If this request is successful (status code 200) our upload process is finished. After we make this request we’ll get a final video URL inside the Location header.

Challenges we faced

Video metadata

After the upload process is finished we can edit the video metadata, which includes video privacy, video title, information about the video, customization of the Vimeo player, custom thumbnails, etc.

The next thing we need to do is set the video privacy to ‘unlisted’, as this option best suits our business requirements (that is why we needed edit permission for our Vimeo API token). We can do this step once we have the video ID (the last part of the full video URL grabbed from the Location header). We do this by editing the video’s metadata. Here we can also set things like the video title, information about the video, customization of the Vimeo player, add custom thumbnails, etc. The request we need to send is:

PATCH

https://api.vimeo.com/videos/[video_id]

Headers

Authorization: bearer [your_vimeo_token]

Body

{
   “privacy”: {
      “view”: “unlisted”
   }
}
// Request
@Headers("Authorization: bearer [your_vimeo_token]")
@PATCH("/videos/{video_id}")
Call<VimeoVideo> editVideoMetadata(@Path("video_id") String videoId, @Body VideoMetadata videoMetadata);

// Call
VideoMetadata metadata = new VideoMetadata("unlisted");
Call<VimeoVideo> call = vimeoApi.editVideoMetadata(videoId, metadata);
call.enqueue(new Callback<VimeoVideo>() {
    @Override
    public void onResponse(Call<VimeoVideo> call, Response<VimeoVideo> response) {
        // Here we can notify server that video upload process finished
    }

    @Override
    public void onFailure(Call<VimeoVideo> call, Throwable t) {
        // Edit of video metadata failed
    }
});
-(void)callCompleteUrl:(NSString *)completeUrl {

    NSString *urlString =[NSString stringWithFormat:@"%@%@", VimeoApiBaseUrl, completeUrl];
    NSMutableURLRequest *request=[NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlString] cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:0];
    [request setHTTPMethod:@"DELETE"]
    [request setValue:VimeoAuthorization forHTTPHeaderField:@"Authorization"];

    [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
        NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response;
        if (!error) {
            // Handle success
        }
        else {
            // Handle failure
        }
    }];
}

If we get status code 200 we know that everything has gone ok.

Video processing and caching

While the video is processing, the Vimeo is doing multiple things – creating multiple sources of quality for video, thumbnails and compression. After the video has finished processing we can cache information about the video to our API.

In this phase Vimeo will:

  • generate multiple mp3 sources for different qualities of video
  • generate an HLS source (that we will use)
  • generate multiple sizes of thumbnails
  • compress the video (for faster load time).

The problematic element of the processing is that we don’t know how long it will take. Processing time is dependent on the size of our video and also the Vimeo account plan we are using. Depending on the number of videos that need to be processed, Vimeo will give higher priority to the more expensive plans – so don’t worry if the processing takes quite a while on the free Vimeo plan. We can find out if the video has finished processing by sending the following request:

GET

https://api.vimeo.com/videos/[video_id]?fields=status,files,pictures

Headers

Authorization: bearer [your_vimeo_token]

In the response we will get a status field we can check for the value available to determine whether the processing has finished.

For our project we want a single response from the Vimeo API containing all the information we need to show a user gallery of videos and photos, so we need to inform the server of our HLS link and thumbnail URL as soon as any client (Web, Android or iOS) detects that the processing has finished.

When the video has finished processing we can dig through the JSON response to get our HLS link by accessing the files attribute, which is a list of objects. Every object in that list has the attribute quality. We need to find the object where quality matches hls; then we can access the HLS link inside the link_secure attribute.

For our thumbnail we can dig through the same JSON response and access pictures and it’s property sizes which is list of objects. We can access the first objects link property to get a link to the thumbnail. The next thing we need to do is subtract that string until the last “_” character. When we want to generate a thumbnail we just need to concatenate the link with, for example, 200×200 if we want to get an image of width and height 200px.

Rate limiting

Rate limiting is a per user limitation relating to the number of API calls. We can handle not blowing up the Vimeo API by implementing caching and API calls optimization.

A very important ‘gotcha’ that you need to know when working with Vimeo is rate limiting. Every application on Vimeo has a set amount of requests we can send to the Vimeo API before the API blocks our application. We need to keep this in mind while developing, so that we don’t ping the Vimeo API an unreasonable number of times. The rate limit for our application is calculated dynamically and is in the range of 100 to 2000.

We need to optimize our Vimeo API calls in order to increase the rate limit. This is why we use the fields parameter, to keep our API transactions to a minimum. Another thing Vimeo suggests is caching the information on our API. With these things in place we can be comfortable that we will not exceed the Vimeo API’s rate limit.

If we want to check how much of our rate limit we’ve used, we can check the next headers of any of our API responses:
– X-RateLimit-Limit – value of our rate limit
– X-RateLimit-Remaining – value of the amount of rate limit we have left
– X-RateLimit-Reset – date and time value of when the rate limit is reset.

3 Comments

  • Faheem Parker says:

    for these steps, do we need any libraries from your team? What I understand is we don’t… Between many thanks for this tutorial… It saved a lot of time…

    • I’m very happy that this blog was helpful to you. You are right, no special libraries are necessary for either platform, we just used standard networking libraries for Android (Retrofit) and iOS (RestKit).

  • sushant says:

    Able to create the ticket but not able to upload video..On given PUT api..doesn’t get any response.
    @PUT(“/upload”)
    fun uploadVideo(@Query(“ticket_id”) ticketId: String?,
    @Query(“video_file_id”) videoId: String?,
    @Query(“signature”) signature: String?,
    @Query(“v6”) version: Int?,
    @HeaderMap headers: Map,
    @Body video: VimeoTicketRequest): Call

Leave a Reply