It’s a long story. Encrypting a video and playing it in android I found it very difficult. There are no proper documents and tutorials for it. At that time it took me almost 1.5 months to figure out how it works.
So today I will explain every step that needs to be taken to play an encrypted video in android exoplayer and try to write a complete tutorial of it so that any noobs like me can get help from this post.
Explanation of the strategies:
First we will encrypt the video using FFMPEG. Then we will upload encrypted video to our server. After that we will create an api from where exoplayer can get the decryption key. And lastly we will implement the exoplayer in the android project.
** note: Install ffmpeg in your system if it’s not installed and make sure it can be executable from the terminal . You can download it from here: https://ffmpeg.org/download.html
Step 1: Creating keys
Let’s create a folder and name it exoEncTutorial. In this folder create 2 files. Name those keyinfo.txt and decrypt.key accordingly. In the keyinfo file we have to put 2 pieces of information.
Info 1
Info 1 is the API link which is a HTTP GET method. Exoplayer will call this link to retrieve the key. I have another domain named https://foxytool.com/. I will host the API link there.
Info 2:
Info 2 is the path of the .key file. You have to put the path of your decrypt.key file in keyinfo.txt.
Here is my keyinfo.txt file for example.
https:foxytool.com/getDecryptKey
/Users/tusharmonirul/Desktop/Dev/Tushar/ffmpeg/decrypt.key
Now open your decrypt.key file. Here simply write a 16 characters password or key. Make sure you put multiple special characters in it.
Here is the password/key of my decrypt.key file:
hunnybunny!@#$%^
Remember it must have to be 16 or 24 or 32 characters long.
Step 2: Encrypt the video
It’s time to encrypt your target video. Let’s say the file name is example.mp4. Copy the video file and paste it in the folder exoEncTutorial you created recently. Open your terminal and cd to exoEncTutorial. Run the below command:
ffmpeg -i example.mp4 -hls_time 10 -hls_list_size 0 -hls_key_info_file keyinfo.txt arc.m3u8
When you run this command ffmpeg will break your videos into chunks and encrypt it. Each chunk will be 10 seconds long because we used -hls_time 10. It actually converts your video into standard MPEG-2(.ts). You will find a .m3u8 file also named arc.m3u8. This is a playlist containing all the instructions that a video player can read to play the chunks. Open this file and you will see something like below:
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:17
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-KEY:METHOD=AES-128,URI="https://foxytool.com/getDecryptKey",IV=0x00000000000000000000000000000000
#EXTINF:16.666667,
arc0.ts
#EXTINF:8.333333,
arc1.ts
#EXTINF:5.033333,
arc2.ts
#EXT-X-ENDLIST
Notice the line #EXT-X-KEY:METHOD=AES-128,URI="https://foxytool.com/getDecryptKey",IV=0x00000000000000000000000000000000
It is actually saying that the video is encrypted using AES-128
method and the decryption key can be found at https://foxytool.com/getDecryptKey
.
Step 3: Upload encrypted videos
Now it’s time to upload these files into our server. Zip your newly created .m3u8 and all .ts files. Upload the zip file into your cpanel hosting or vps. Then unzip the file. Make sure the .m3u8 and the .ts files are in the same folder and they are accessible via hotlink.
For example:
https://yourdomain/videoName/video.m3u8 or
https://yourdomain/videoName/video.ts or
https://yourdomain/videoName/video1.ts
Step 4: Enable CORS
After uploading the files you must have to enable CORS so that the video player can request the files. To enable the CORS, it depends on your server system. If your server runs on Apache you need to add .htaccess
which will contain the below code:
<IfModule mod_headers.c>
Header set Access-Control-Allow-Origin "*"
</IfModule>
And if it is Ngnix
then you have to add following code in your Ngnix
default configuration:
location /yourRoute/ {
alias /location/of/your/folder/;
add_header 'Access-Control-Allow-Origin' '*' always;
}
If you are using cpanel, then I think you have use .htaccess
Step 5: Implement the decrypt key api
Remember we used a decryption link in step 2 during encrypting the video. It’s time to implement the api/link. As I have told you that I used spring boot, I will show you the code which I have written in java. But please don’t worry about it. You can write it in any language.
So when you try to play your encrypted video in exoplayer, it will send a HTTP GET request to your link which is defined in .m3u8. Now how do you validate the request? How do you determine that the request is coming from your app and a valid user? Well, you need to check the header in the request. You will find thekey value pairs in the header which is sent by exoplayer. But remember, of course you will have to tell the exoplayer what value needs to be sent.
After checking all the values if you find the request has come from a valid user, you can simply return the key. Following code is an example. You can have the idea from it.
@ResponseBody
@RequestMapping(value="/getDecryptKey", method = RequestMethod.GET)
public String getDecryptKey(@RequestHeader Map headers){
String userId = headers.get("userId");
String session = headers.get("timeOutSession");
String token = headers.get("userAuthToken");
if(!doesUserHaveAccess(userId)){
return "You dont have access";
}
if(!isValidSession(session)){
return "Your session expired";
}
if(!isValidToken(token)){
return "Your token is not valid";
}
return "hunnybunny!@#$%^";
}
Step 6: Implement Exoplayer
Add the following libraries to install exoplayer in your project.
implementation 'com.google.android.exoplayer:exoplayer:2.11.7'
implementation 'com.google.android.exoplayer:exoplayer-hls:2.11.7'
Now create a new activity and name it PlayerActivity. Add the following code:
package com.captaindroid.exoplayerencryptedvideo;
import android.net.Uri;
import android.os.Bundle;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import com.captaindroid.exoplayerencryptedvideo.databinding.ActivityMainBinding;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.DefaultRenderersFactory;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.RenderersFactory;
import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.audio.AudioAttributes;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.MappingTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
import com.google.android.exoplayer2.util.Util;
public class PlayerActivity extends AppCompatActivity {
private ActivityPlayerBinding binding;
private DefaultTrackSelector trackSelector;
private DefaultTrackSelector.Parameters trackSelectorParameters;
private TrackGroupArray lastSeenTrackGroupArray;
private SimpleExoPlayer player;
private String videoURL = "https://foxytool.com/file/arc.m3u8";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
initializePlayer(videoURL);
}
private void initializePlayer(String videoUrl) {
TrackSelection.Factory trackSelectionFactory = new AdaptiveTrackSelection.Factory();
RenderersFactory renderersFactory = buildRenderersFactory(false);
trackSelector = new DefaultTrackSelector(/* context= */ this, trackSelectionFactory);
DefaultTrackSelector.ParametersBuilder builder = new DefaultTrackSelector.ParametersBuilder(/* context= */ this);
trackSelectorParameters = builder.build();
trackSelector.setParameters(trackSelector
.buildUponParameters().setAllowVideoMixedMimeTypeAdaptiveness(true)
.setMaxVideoSizeSd()
.setPreferredTextLanguage("en")
.setPreferredAudioLanguage("en"));
lastSeenTrackGroupArray = null;
player = new SimpleExoPlayer.Builder(this, renderersFactory).setTrackSelector(trackSelector).build();
player.setAudioAttributes(AudioAttributes.DEFAULT);
//DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(this, Util.getUserAgent(this, getString(R.string.app_name)));
DefaultHttpDataSourceFactory dataSourceFactory = new
DefaultHttpDataSourceFactory(Util.getUserAgent(this, getString(R.string.app_name)), null);
dataSourceFactory.getDefaultRequestProperties().set("userId", yourUserId);
dataSourceFactory.getDefaultRequestProperties().set("timeOutSession", yourTimeOutSession);
dataSourceFactory.getDefaultRequestProperties().set("userAuthToken", yourUserAuthToken);
//MediaSource videoSource = new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(Uri.parse(videoUrl));
MediaSource videoSource = new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(Uri.parse(videoUrl));
//MediaSource videoSource = new ExtractorMediaSource(Uri.parse(videoUrl), new ExoPlayerCacheDataSourceFactory(this, 100 * 1024 * 1024, 5 * 1024 * 1024), new DefaultExtractorsFactory(), null, null);
player.prepare(videoSource);
player.setSeekParameters(SeekParameters.CLOSEST_SYNC);
player.setPlayWhenReady(true);
player.addListener(new Player.EventListener() {
@Override
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
if (trackGroups != lastSeenTrackGroupArray) {
MappingTrackSelector.MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo();
if (mappedTrackInfo != null) {
if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_VIDEO) == MappingTrackSelector.MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
Toast.makeText(MainActivity.this, "video unsupported", Toast.LENGTH_LONG).show();
}
if (mappedTrackInfo.getTypeSupport(C.TRACK_TYPE_AUDIO) == MappingTrackSelector.MappedTrackInfo.RENDERER_SUPPORT_UNSUPPORTED_TRACKS) {
Toast.makeText(MainActivity.this, "audio unsupported", Toast.LENGTH_LONG).show();
}
}
lastSeenTrackGroupArray = trackGroups;
}
}
});
binding.player.setPlayer(player);
}
private RenderersFactory buildRenderersFactory(boolean preferExtensionRenderer) {
@DefaultRenderersFactory.ExtensionRendererMode
int extensionRendererMode =
true
? (preferExtensionRenderer
? DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON)
: DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF;
return new DefaultRenderersFactory(/* context= */ this)
.setExtensionRendererMode(extensionRendererMode);
}
}
The layout:
<?xml version="1.0" encoding="UTF-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:keepScreenOn="true">
<com.google.android.exoplayer2.ui.PlayerView
android:id="@+id/player"
android:layout_width="match_parent"
android:layout_height="250dp"
app:show_buffering="when_playing"
app:show_timeout="0"
app:show_shuffle_button="true"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#88000000"
android:orientation="vertical">
<LinearLayout android:id="@+id/controls_root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="gone">
<Button android:id="@+id/select_tracks_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Title"
android:enabled="false"/>
</LinearLayout>
</LinearLayout>
</FrameLayout>
Step 7: Understanding the Exoplayer process
Please pay attention to the following lines which we added just now:
DefaultHttpDataSourceFactory dataSourceFactory = new DefaultHttpDataSourceFactory(Util.getUserAgent(this, getString(R.string.app_name)), null);
dataSourceFactory.getDefaultRequestProperties().set("userId", yourUserId);
dataSourceFactory.getDefaultRequestProperties().set("timeOutSession", yourTimeOutSession);
dataSourceFactory.getDefaultRequestProperties().set("userAuthToken", yourUserAuthToken);
When we use getDefaultRequestProperties().set(“key”, “value”);
it actually tells exoplayer to carry these key value pairs in the header during the HTTP call. You can use multiple key value pairs, it will carry all of them. Remember the key value pairs do not have to be the same as examples. It depends on you, what kind of data you need to check to validate.
Step 8: Exercise(Optional)
Please try to play the video from this link: https://foxytool.com/file/arc.m3u8.
To get the decryption key use the following data.
username: captaindroid
bearertoken: 65def76d-5f15-4f7d-a17d-d76f1db10df0
If you are able to play the video successfully please let me know in the comment box.