The Android platform supports many media formats.
Are there better choices in the context of the library? Here under are some hints, based on experience. Do not take them as references, but as an aid to make your own opinion.
The most important point is:
For 3GPP and MPEG-4 containers, the
moov
atom must precede anymdat
atoms, but must succeed theftyp
atom.
The mdat atom is a section containing media frames and is the biggest in size. The moov atom is a section for metadata, i.e. properties, indexes, offsets and other structural information. For an encoder, the natural order is to write the mdat first, then the moov, after knowing what the offsets are. For a decoder, it's the opposite: the moov has to be fetched first to know how to interpret the mdat. Which one comes before the other is not that critical with a file on disk, thanks to a direct access. But it really matters for progressive streaming, and even more with contents encrypted with block-chaining algorithms where accessing any part supposes an entire reading from the very beginning.
An inadequate order does not prevent the play, it incurs a delay to the start, proportional to the file size. Some improvements over the native Android implementation have been made in the library (since version 1.3) in order to speed up this step, with a gain up to 5.6 times for AES/CBC and 8.2 times for RC4. But a delay remains: for a rough order of magnitude, count about 2.3 seconds per 10 megabytes for AES/CBC and 1 second per 10 megabytes for RC4 (on a Nexus 7 2012). In addition, in this case and for devices as of Android 4.3, special caution should be given to the selected Security Provider.
--full
option or this specific option: --inform=General;%IsStreamable%
.
In both cases, check that the IsStreamable property is Yes.--info
option
and check the presence of File suitable for progressive download (moov before mdat).
When the file is not suitable, there is no negative mention.-movflags +faststart
Otherwise, use another tool. Here are some examples:
mp4box -inter 500 in.mp4 -out out.mp4
Cipher.getInstance(…)
without specifying the provider, one is chosen among
the registered security providers, in accordance with an order of preference.
Android 4.3 introduced AndroidOpenSSL as the preferred default provider, before BouncyCastle ("BC"). Initially, this new implementation didn't work as expected when the cipher is used a second time. This is particularly true with the atom order issue detailed in Media Encoding.
The bug has been partially fixed in 5.0. There is still a problem with algorithms having a padding. It may help to have a fix if you star the issue.
So it seems safer to specify "BC" as the provider, for devices running version:Unfortunately, the BC provider is deprecated and can't no more be imposed as of Android P (an exception is thrown). So this fallback must be restricted to versions before 9.
See also below some code samples.
New in v3.2.5 In the above mentioned situations, an alternative to the BC provider is to implement a CipherFactory, even if it doesn't seem mandatory otherwise. But it becomes the only solution for padded algorithms as of Android P (anyway, padded algorithms are discouraged, see next section).
However, you can use a block algorithm and select a mode that does not require padding to effectively use a block cipher as a stream cipher.
Here are some examples:In case you don't have any criteria or legacy/compatibility constraints, the RC4 stream cipher is a good candidate, it's a widely used stream cipher and it's less processing intensive than the block ciphers.
There is no obvious solution to this problem. However, among the various block cipher modes of operation, some allow a random read access during decryption.
It doesn't change anything for the repeated hits from the MediaPlayer but at least the response comes quicker.
To benefit from this advantage, you have to:Additionnal information on other modes:
OFB: This mode does not allow a random read access.
ECB: Although suitable for random access, it does not hide data patterns well and is not recommended in cryptographic.
Nevertheless, the library supports (v3.2+)
this mode as well, and there is no need for a CipherFactory.
File file = new File( Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES), "path/to/video.mp4"); String path = mServer.getURL(file.getPath());
String path = mServer.getURL("/path/to/container.zip", "path/to/video.mp4");
int mainVersion = 1; int patchVersion = 0; String path = mServer.getURL(mainVersion, patchVersion, "path/to/video.mp4");
String path = mServer.getURL("asset://hd/video.mp4");
String path = mServer.getURL("smb://server/share/path/to/video.mp4");
String path = mServer.getURL("http://media.mydomain.com/path/to/video.mp4");
String path = mServer.getURL("ftp://ftp.mydomain.com/path/to/video.mp4");
<video>
in WebViewYou can design your application in two basic ways (or even a mix of both):
val webview = WebView(this) webview.settings.javaScriptEnabled = true // mServer = ... var path = "/some/path" path = mServer.getURL(path) webview.webViewClient = object : WebViewClient() { override fun onPageFinished(view: WebView, url: String) { view.loadUrl("javascript:setURL('$path'); void(0);") } } val page = """ <html><body> <script> function setURL(url) { document.getElementById('v_id').src = url; } </script> <video id="v_id" controls></video> </body></html> """ webview.loadData(page, "text/html", null)
final WebView webview = new WebView(this); webview.getSettings().setJavaScriptEnabled(true); // mServer = ... String path = '/some/path'; path = mServer.getURL(path); final String mediaPath = path; // just because 'final' is mandatory webview.setWebViewClient(new WebViewClient() { @Override public void onPageFinished(WebView view, String url) { view.loadUrl("javascript:setURL('" + mediaPath + "'); void(0);"); } }); String page = "<html><body>" + "<script>" + "function setURL(url) {" + " document.getElementById('v_id').src = url; }" + "</script>" + "<video id=\"v_id\" controls></video>" + "</body></html>"; webview.loadData(page, "text/html", null);
val webview = WebView(this) webview.settings.javaScriptEnabled = true // mServer = ... webview.addJavascriptInterface( mServer.jsInterfaceObject(), "serverObject") val page = """ <html><body> <script> function setURL(url) { document.getElementById('v_id').src = url; } function setPath(path) { setURL(serverObject.getURL(path)); } // ... logic for: // setPath('/some/path'); </script> <video id="v_id" controls></video> </body></html> """ webview.loadData(page, "text/html", null);
final WebView webview = new WebView(this); webview.getSettings().setJavaScriptEnabled(true); // mServer = ... webview.addJavascriptInterface( mServer.getJsInterfaceObject(), "serverObject"); String page = "<html><body>" + "<script>" + "function setURL(url) {" + " document.getElementById('v_id').src = url; }" + "function setPath(path) {" + " setURL(serverObject.getURL(path)); }" + "// ... logic for:" + "// setPath('/some/path');" + "</script>" + "<video id=\"v_id\" controls></video>" + "</body></html>"; webview.loadData(page, "text/html", null);
A partial sample of how to play an encrypted video in a VideoView widget:
private fun myPlay(path: String) { mServer = LocalSingleHttpServer() mServer!!.setCipher(cipher) mServer!!.start() path = mServer!!.getURL(path) mVideoView.setVideoPath(path) mVideoView.start() } override fun onCompletion(mp: MediaPlayer) { // MediaPlayer.OnCompletionListener interface mServer!!.stop() mServer = null } private val cipher: Cipher? @Throws(GeneralSecurityException::class) get() { val algorithm = "AES" val transformation = "AES/CTR/NoPadding" val key = "1234567890123456" val iv = ByteArray(16) val provider = selectProvider(transformation) val c = if (provider != null) Cipher.getInstance(transformation, provider) else Cipher.getInstance(transformation) c.init(Cipher.DECRYPT_MODE, SecretKeySpec(key.toByteArray(), algorithm), IvParameterSpec(iv))) return c } companion object { private fun selectProvider(transformation: String): String? { /* Avoid the default security provider "AndroidOpenSSL" in some cases between Android 4.3 and 9 http://libeasy.alwaysdata.net/network/#provider */ val ucName = transformation.toUpperCase() return if (Build.VERSION_CODES.JELLY_BEAN_MR2 <= Build.VERSION.SDK_INT && (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP || (ucName.endsWith("PADDING") && !ucName.endsWith("/NOPADDING") && Build.VERSION.SDK_INT < Build.VERSION_CODES.P))) "BC" else null } }
private void myPlay(String path) { mServer = new LocalSingleHttpServer(); mServer.setCipher(myGetCipher()); mServer.start(); path = mServer.getURL(path); mVideoView.setVideoPath(path); mVideoView.start(); } public void onCompletion(MediaPlayer mp) { // MediaPlayer.OnCompletionListener interface mServer.stop(); mServer = null; } private Cipher myGetCipher() { final String algorithm = "AES"; final String transformation = "AES/CTR/NoPadding"; final String key = "1234567890123456"; final byte[] iv = new byte[16]; final String provider = selectProvider(transformation); final Cipher c = provider != null ? Cipher.getInstance(transformation, provider) : Cipher.getInstance(transformation); c.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key.getBytes(), algorithm), new IvParameterSpec(iv); return c; } private static String selectProvider(final String transformation) { /* Avoid the default security provider "AndroidOpenSSL" in some cases between Android 4.3 and 9 http://libeasy.alwaysdata.net/network/#provider */ final String ucName = transformation.toUpperCase(); return (Build.VERSION_CODES.JELLY_BEAN_MR2 <= Build.VERSION.SDK_INT && (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP || (ucName.endsWith("PADDING") && !ucName.endsWith("/NOPADDING") && Build.VERSION.SDK_INT < Build.VERSION_CODES.P))) ? "BC" : null; }
A partial sample of how to implement a CipherFactory:
private fun myPlay(path: String) { mServer = LocalSingleHttpServer() mServer!!.setCipherFactory(MyCipherFactory()) mServer!!.start() path = mServer!!.getURL(path) mVideoView.setVideoPath(path) mVideoView.start() } override fun onCompletion(mp: MediaPlayer) { // MediaPlayer.OnCompletionListener interface mServer!!.stop() mServer = null }
private void myPlay(String path) { mServer = new LocalSingleHttpServer(); mServer.setCipherFactory(new MyCipherFactory()); mServer.start(); path = mServer.getURL(path); mVideoView.setVideoPath(path); mVideoView.start(); } public void onCompletion(MediaPlayer mp) { // MediaPlayer.OnCompletionListener interface mServer.stop(); mServer = null; }
private class MyCipherFactory : CipherFactory { @Throws(GeneralSecurityException::class) override fun getCipher(): Cipher { // you are free to choose your own Initialization Vector val initialIV = ByteArray(16) return rebaseCipher(initialIV) } @Throws(GeneralSecurityException::class) override fun rebaseCipher(iv: ByteArray): Cipher { val c = Cipher.getInstance("AES/CTR/NoPadding") c.init(Cipher.DECRYPT_MODE, SecretKeySpec("1234567890123456".toByteArray(), "AES"), IvParameterSpec(iv)) return c } }
private class MyCipherFactory implements CipherFactory { @Override public Cipher getCipher() throws GeneralSecurityException { // you are free to choose your own Initialization Vector byte[] initialIV = new byte[16]; return rebaseCipher(initialIV); } @Override public Cipher rebaseCipher(byte[] iv) throws GeneralSecurityException { final Cipher c = Cipher.getInstance("AES/CTR/NoPadding"); c.init(Cipher.DECRYPT_MODE, new SecretKeySpec("1234567890123456".getBytes(), "AES"), new IvParameterSpec(iv)); return c; } }
private class MyCipherFactory implements LocalSingleHttpServer.CipherFactory { // ... same as v3
private class MyCipherFactory implements LocalSingleHttpServer.CipherFactory { @Override public Cipher getCipher() throws IOException { // you are free to choose your own Initialization Vector byte[] initialIV = new byte[16]; return rebaseCipher(initialIV); } @Override public Cipher rebaseCipher(byte[] iv) throws IOException { Cipher c = null; try { // avoid the default security provider "AndroidOpenSSL" in Android 4.3+ (http://libeasy.alwaysdata.net/network/#provider) c = Cipher.getInstance("AES/CTR/NoPadding", "BC"); c.init(Cipher.DECRYPT_MODE, new SecretKeySpec("1234567890123456".getBytes(), "AES"), new IvParameterSpec(iv)); } catch (GeneralSecurityException e) { throw new IOException("Unable to create a cipher", e); } return c; } }
- public static void encrypt() throws Exception {
- final byte[] buf = new byte[8192];
- final Cipher c = Cipher.getInstance("AES/CTR/NoPadding");
- c.init(Cipher.ENCRYPT_MODE, new SecretKeySpec("1234567890123456".getBytes(), "AES"), new IvParameterSpec(new byte[16]));
- final InputStream is = new FileInputStream("/path/to/src/video.mp4");
- final OutputStream os = new CipherOutputStream(new FileOutputStream("/path/to/dst/video.mp4"), c);
- while (true) {
- int n = is.read(buf);
- if (n == -1) break;
- os.write(buf, 0, n);
- }
- os.close(); is.close();
- }
- var crypto = require('crypto');
- var algorithm = 'aes-128-ctr';
- var key = new Buffer('1234567890123456');
- var iv = new Buffer('\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0');
- var c = crypto.createCipheriv(algorithm, key, iv);
-
- var fs = require('fs');
- var r = fs.createReadStream('/path/to/src/video.mp4');
- var w = fs.createWriteStream('/path/to/dst/video.mp4');
- r.pipe(c).pipe(w);
Note: code checked with M2Crypto version 0.35.2.
- from M2Crypto.RC4 import RC4
-
- key = bytes('BrianIsInTheKitchen', 'utf-8')
- cipher = RC4(key)
-
- src_path = '/path/to/src/video.mp4'
- dst_path = '/path/to/dst/video.mp4'
- with open(src_path, 'rb') as input, open(dst_path, 'wb') as output:
- output.write(cipher.update(input.read())) # no need of cipher.final() for RC4
openssl enc -aes-128-ctr -in /path/to/src/video.mp4 -out /path/to/dst/video.mp4 -K 000102030405060708090A0B0C0D0E0F -iv 0
No. We are focused on Android only. A React Native module is also available (again, for Android build only).
Yes. The decrypted stream can be delivered to a <video>
or <audio>
tag
in a WebView, or to a Chromecast device.
Note that it is not a thrown exception but a Debug message from the MediaPlayer, detailed with a stacktrace.
It's a normal behavior of MediaPlayer: whatever the path content is, it first tries it as a local resource
and if it fails it will fallback to a remote resource.
You see that on the next Debug message:
"D/MediaPlayer: Couldn't open file on client side, trying server side"
Do you intend to use any of these optional data sources: SMB server or APK Expansion Zip file?
If not, just ignore the error, it doesn't prevent the app to work.
Otherwise, you forgot to embed an additional library in your app,
so expect the dedicated getURL()
methods to return null.
A possible explanation is the setting of a proxy on the network interface, which routes all traffic to outside, even for localhost requests. On the Wifi interface, there is a "Bypass proxy for" option where "127.0.0.1" can be entered. Unfortunately this is not the case for mobile interface: in the Edit screen of an Access Point, you can find a Proxy and Port but no bypass input field.
The solution is to unset the Proxy setting on the mobile network interface.