赞
踩
目前针对一些有价值的片源,大家基本或多或少都会用到点加密技术,最简单的比如修改MP4文件头。到了要播放的时候,就需要找到一种比较方便的解密–播放方法,简单来说,就是在Android应用上实现对本地多媒体文件一边解密一边播放的功能。
考虑到需求是一边加密一边解密,那么像是在本地先读文件流解密后再输出清流播放这类方法肯定是不行了,因为会产生无加密的中间文件,理论上应该是一边读取文件流,一边解密,然后将解密的文件流直接播放。
MediaPlayer
能够播放的源说白了就2种:网络流媒体、本地文件。本地文件播放不能满足上述需求,网络流媒体播放显然是可以的。那么我们的目标就在于,如何将本地文件转换为流媒体让MediaPlayer
进行播放?
先让我们搞明白MediaPlayer
是如何播放网络流媒体的。
尝试用模拟器播放一个网络流,再配合tcpdump+wireshark,可以看到MediaPlayer
发送了一个Http Get
请求给服务器端,而服务器端则返回了相应的Http Reponse
及文件数据,这些数据就是被播放出来的音视频。
因此,这意味着我们可以在本地创建一个最简单的服务器实现,接收MediaPlayer
的请求,读取本地多媒体文件,转为文件流进行解密,然后将数据流返回给MediaPlayer
进行播放,即可达到边解密边播放的效果。
1.创建Local Socket服务端
我选择使用NIO来实现,首先监听端口,创建Selector
。在新线程内等待客户端的通讯并做出响应。
public MediaProxyServer(int port) {
try {
isStop = new AtomicBoolean(false);
isNeedReponse = new AtomicBoolean(false);
selector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
serverChannel.socket().bind(new InetSocketAddress(port), 1024);
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
e.printStackTrace();
}
}
2.读取客户端请求
在这个简单示例里,我们只关心MediaPlayer
的GET
请求。
if (key.isReadable()) {
String content = readFromClient(key);
if (isHttpGetRequest(content)) {
range = parseRange(content);
isNeedReponse.set(true);
key.channel().register(selector, SelectionKey.OP_WRITE);
}
} // read end
3.读取多媒体文件
之所以使用RandomAccessFile
来打开文件,是为了便于读取文件流时指定开始/结束位置,实现多媒体播放时拖动进度条的效果。
private void openFileChannel() throws FileNotFoundException {
if (null == fileChannel) {
File test = new File(Environment.getExternalStorageDirectory()
.getAbsolutePath()
+ File.separator
+ "Movies"
+ File.separator + "test.mp4");
fileChannel = new RandomAccessFile(test, "r").getChannel();
}
}
4.设置Reponse
Reponse
只需要收到GET
请求之后发送一次。每次发送时根据 GET
请求中的Range
头来确定要返回的数据范围。
private void writeHttpResponse(SelectionKey key, long size, long range) throws IOException {
if (!isNeedReponse.get()) {
return;
}
isNeedReponse.set(false);
StringBuffer sb = new StringBuffer();
sb.append("HTTP/1.1 206 Partial Content\r\n");
sb.append("Content-Type: video/mp4\r\n");
sb.append("Connection: Keep-Alive\r\n");
sb.append("Accept-Ranges: bytes\r\n");
sb.append("Content-Length: " + size + "\r\n");
sb.append("Content-Range: bytes " + range + "-" + (size - 1) + "/" + size + "\r\n");
sb.append("\r\n");
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
buffer.clear();
buffer.put(sb.toString().getBytes());
buffer.flip();
while (buffer.hasRemaining()) {
channel.write(buffer);
}
buffer.clear();
}
5.返回数据
private void writeFileContent(SelectionKey key, FileChannel fileChannel, long range) throws IOException {
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
buffer.clear();
fileChannel.position(range);
while (true) {
if (buffer.hasRemaining()) {
int bytes = fileChannel.read(buffer);
if (-1 == bytes) {
buffer.flip();
while (buffer.hasRemaining()) {
sc.write(buffer);
}
buffer.clear();
// close connect with client
fileChannel.close();
key.cancel();
sc.close();
break;
}
} else {
buffer.flip();
while (buffer.hasRemaining()) {
sc.write(buffer);
}
buffer.clear();
}
}
}
数据不完整
遇到最大的问题是发送数据时,服务端发送量和客户端接受量不相同。最终发现是由于服务端发送数据时,ByteBuffer
有时没有存满就开始对客户端进行发送,发送时ByteBuffer
还有剩余数据时又开始接收文件流中新数据。
解决方法就如上面的代码,使用了两个循环,一是确保读文件读到ByteBuffer
满再发送,二是确保发送时彻底清空ByteBuffer
再去读文件。
响应头设置
MediaPlayer
在播放中会有多次请求,每一次都请求一部分数据。刚开始每次播放都会在第二次请求时停止,最终发现是服务端响应头设置的问题。
首先,Accept-Ranges: bytes
是必须的。
其次,Content-Range: bytes 1-2/3
也是必须的,此处1-2
是返回数据的范围,起始值就是从客户端请求头中解析到的Range
,终止值为了简单可以设置为本地媒体文件的size-1
,3
就是本地文件的size
了。
新请求瞬间播放卡顿
播放器进行新的请求时,服务端要先返回应头,再返回数据,导致在虚拟机上出现瞬间的卡顿。这个问题目前没详细DEBUG,也不太清楚在真机上是否表现一样。
我认为肯定是可以避免的。目前猜测将服务端按照HLS
协议实现可能可以解决。服务端构造M3U8
描述文件,定义每个分片的时间&数据,然后用RandomAccessFile
读取返回。
关于android视频边解密边播放
Android MediaPlayer与Http Proxy结合之基础篇
Android ServerSocket programming with jCIFS streaming files
Java NIO系列教程
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。