Improving Video Playback on Android
As Instagram has grown, and mobile network bandwidth has improved, video has become a larger part of the app experience. The time people spent watching video in the last half increased by more than 40%, and yesterday, we announced longer video to give creators more flexibility and open up new ways to connect with the community. In this post, we’ll share some of the things we’ve done to improve video playback on Android and maintain user experience while increasing video length 4x.
Instagram introduced 15 second video in 2013. One of our core engineering values is “Do the Simple Thing First,” so we implemented a basic video playback mechanism on Android. When a video appears in feed, the app first downloads the whole video file to the phone storage. After the download is complete, the file is passed to the Android built-in MediaPlayer to play the video.

This design worked well when the maximum video length was only 15 seconds. However, we recently decided to increase the maximum video length to 60 seconds. We recognized this would stress our current design for a few reasons:
- Video cannot play until the whole file is downloaded. As the video becomes larger, playback would wait even longer for the video to download. People on slower network connections might need to wait for a very long time before they can play the first second of the video.
- Video won’t play if there’s a disk space issue. If there is not enough space in the disk to store the whole video file in the disk cache or if there’s some failure in the I/O, the video will never play.
With those issues in mind, we decided to build a new video cache that could stream the video to the MediaPlayer as the video file is being downloaded. This would remove the bottleneck of waiting for the download to finish.
We started to look into how to build a streaming video cache that worked with our current video player. We found that the Android MediaPlayer only supports playing a video from either a file or a URL. We wouldn’t have the complete video file to give to the MediaPlayer when we are still downloading, so this leaves us with playing the video from a URL. The naive approach of passing our CDN URL to the MediaPlayer would mean that we wouldn’t have any control over the bytes downloaded, so we would not be able to cache the video in case a person wanted to watch it again. Knowing this fact, we decided to have the MediaPlayer interact with a local proxy server instead, and have the proxy server serve the byte stream to the media player while also storing it into the disk cache.

It turns out that this new design solves many of the problems in the previous design with some extra benefits:
- Playback is no longer blocked by the video download, i.e. it plays as soon as there’s enough content.
- Playback is no longer dependent on disk space. Even if the disk cache malfunctions, it would just be treated as a cache miss and streamed directly from the server.
- Allows more prefetching adjustments. This new mechanism allows us to explore more options in regard to prefetching video files depending on the network condition, as the prefetched content can now be directly streamed while we download the rest of the file on background.
We tested our new implementation extensively across many devices and Android versions. Our testing surfaced an issue: Some Android Lollipop devices made multiple (up to 3) download requests before playing the video — they would request the beginning of the file, then make a HTTP range request for the end of the file, then request the beginning again. To figure this out, we need to understand a little about the mp4 file format. An mp4 is a container for one or more video and audio tracks, and it contains metadata that specifies where each of these tracks start. We had already reconfigured our video encoding to place this metadata at the start of the file, instead of the end (the default), to avoid the inefficient behavior of making multiple requests to play the video. We hypothesized that the MediaPlayer was not playing nice with our video encoding.
Upon deeper investigation, we managed to scope the problem down to devices that use NuPlayer, which is the default implementation of MediaPlayer starting in Lollipop (replacing the older AwesomePlayer). Looking deeper into the MPEG4Extractor and NuCachedSource2 code, we deduced that NuPlayer was not handling one of the MP4 container atoms (specifically, the FREE atom) correctly and was getting confused. Removing FREE atoms solved the problem, and we modified our video transcoding tier to include this cleanup logic before we launched the new video playback. We were pretty satisfied with the result of the new implementation.
Below are some of the (statistically significant) results we gathered after performing a limited A/B test of streaming vs. non-streaming playback with 15-second videos in the wild:

The table above shows three metrics: percentage of videos played within 1 second of scrolling on screen, the average start delay to the first frame of the video, and the 95th percentile of the start delay distribution. We initially started by gathering data on the number of videos played within 1 second and the average start delay for all videos. We noticed that the new streaming cache performed worse for the number of videos that played within 1 second (0.8% fewer videos), but better for average time to play: 18% lower! Since these results contradicted each other, we dug in to analyze the data further.
We sliced the data to exclude videos that started playing within 200ms. The majority of the videos that play within 200ms are probably cached fully on disk, which shouldn’t be affected by our new implementation. We hypothesized that removing the data “noise” from these cached videos would help us get a more accurate measurement of the impact of the streaming player, and help us to understand the discrepancy in performance between less than 1 second video play time vs. the average. Removing the lower end of the distribution gave us a more accurate view of the impact of the streaming player: the average and p95 time to play are faster by 22–28%. However, this still didn’t explain why the number of videos that played within 1 second was actually lower with streaming enabled. We dug in deeper still and plotted the time to play distribution of the videos:

This plot shows the percentage of videos (y-axis) that are played within a certain number of seconds (x-axis). Looking at the distribution, we see that the streaming cache (black bars) improved the start time in general, but it seemed to regress the start time for videos that had formerly fallen into the 0–1 second bucket. To try to explain this result, we circled back to check the implementation of MediaPlayer. We discovered that using the setUrl API incurs an artificial 1 second start delay on some older versions of Android MediaPlayer (to give the streaming buffer extra time to fill). This matches the data plot above and explains the result we gathered. In general, the streaming cache has shifted the distribution to the left (you can see the long tail of only blue bars), but pushes some of the leftmost distribution slightly to the right. The impact of this artificial delay is minimal, because we wrote code to shortcut the proxy server and play the file directly if it is fully cached (the setFile API does not have a delay).
We did the simple thing first when we first launched video on Instagram, and shipped a stable and robust video-viewing experience to our users in a timely manner. But doing the simple thing first does not mean that we sacrifice quality or performance. On the contrary, we focus on what matters most at the time and continuously reevaluate what we have to make sure that the whole app experience is top-notch. Video playback is one recent example where changing product requirements led us to rethink our initial assumptions. It also showcases the importance of collecting and analyzing data to verify that performance improvements are actually working as expected! We hope that by providing a better video playback experience we will enable more people to enjoy the moments that they have captured and shared on Instagram.