解决思路:
1、通过网址读取mp4流的关键字来判断ftyp、free、mdat、moov。新建文件destFile,然后:
a、下载ftyp的全部到newFile
b、下载moov全部到newFile
c、写mdat大小的空白数据到newFIle
d、等b和c都完成之后(因b和c这两步的先后不确定),再重新定位mp4流到mdat部分,下载56k(大小可以自行设定,这里我设的是56k)的数据到newFile,然后通知播放端开始播放视频(就用MediaPlayer.setDataSource(newfile)方法),后台下载端继续将最后部分mdat数据完全下载到newFile,即完成mp4数据的下载
代码如下
import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
/**
* Created by Administrator on 2017/11/16.
*/
public class Mp4DownloadUtils {
/**
* 播放MP4消息
*/
public static final int PLAYER_MP4_MSG = 0x1001;
/**
* 下载MP4完成
*/
public static final int DOWNLOAD_MP4_COMPLETION = 0x1002;
/**
* 下载MP4失败
*/
public static final int DOWNLOAD_MP4_FAIL = 0x1003;
/**
* 下载MP4进度
*/
public static final int DOWNLOAD_MP4_LOADING_PROCESS = 0x1004;
/**
* 下载MP4文件
*
* @param url
* @param fileName
* @param handler
* @return
*/
public static File downloadMp4File(final String url, final String fileName,
final Handler handler) {
final File mp4File = new File(fileName);
downloadVideoToFile(url, mp4File, handler);
return mp4File;
}
/**
* 下载视频数据到文件
*
* @param url
* @param dstFile
*/
private static final int BUFFER_SIZE = 4 * 1024;
static boolean isRunning = true;
static Thread thread;
public static void kill(){
if (thread!=null){
isRunning = false;
thread.interrupt();
}
}
/**
* @param url:mp4文件url
* @param dstFile:下载后缓存文件
* @param handler: 通知UI主线程的handler
*/
private static void downloadVideoToFile(final String url, final File dstFile, final Handler handler) {
isRunning = true;
thread = new Thread() {
InputStream is = null;
RandomAccessFile raf = null;
BufferedInputStream bis = null;
@Override
public void run() {
super.run();
try {
URL request = new URL(url);
HttpURLConnection httpConn = (HttpURLConnection) request.openConnection();
httpConn.setConnectTimeout(3000);
httpConn.setDefaultUseCaches(false);
httpConn.setRequestMethod("GET");
// httpConn.setRequestProperty("Charset", "UTF-8");
// httpConn.setRequestProperty("Accept-Encoding", "identity");
int responseCode = httpConn.getResponseCode();
Log.d("chy", responseCode + "");
if ((responseCode == HttpURLConnection.HTTP_OK)) {//链接成功
// 获取文件总长度
// int totalLength = httpConn.getContentLength();
is = httpConn.getInputStream();
if (dstFile.exists()) {
dstFile.delete();
}
//新建缓存文件
dstFile.createNewFile();
raf = new RandomAccessFile(dstFile, "rw");
bis = new BufferedInputStream(is);
int readSize;
//读取Mp4流
int mdatSize = 0;// mp4的mdat长度
int headSize = 0;// mp4从流里已经读取的长度
int mdatMark = 0;//mdat的标记位置
byte[] boxSizeBuf = new byte[4];
byte[] boxTypeBuf = new byte[4];
// 由MP4的文件格式读取
int boxSize = readBoxSize(bis, boxSizeBuf);
String boxType = readBoxType(bis, boxTypeBuf);
raf.write(boxSizeBuf);
raf.write(boxTypeBuf);
boolean isMoovRead = false;
boolean isMdatRead = false;
boolean isftypRead = false;
byte[] buffer = new byte[BUFFER_SIZE];
//判断ftyp、free、mdat、moov 并下载
while (isRunning &&!Thread.currentThread().isInterrupted()) {
int count = boxSize - 8;
if (boxType.equalsIgnoreCase("ftyp")) {
headSize += boxSize;
byte[] ftyps = new byte[count];
bis.read(ftyps, 0, count);
raf.write(ftyps, 0, count);
isftypRead = true;
LogUtils.getInstance().i("ftyp ok");
} else if (boxType.equalsIgnoreCase("mdat")) {
headSize += boxSize;
//正常模式
mdatSize = boxSize - 8;
int dealSize = mdatSize;
//填充mdata大小的空白数据到dstFile,先填充destFile的大小
while (dealSize > 0) {
if (dealSize > BUFFER_SIZE)
raf.write(buffer);
else
raf.write(buffer, 0, dealSize);
dealSize -= BUFFER_SIZE;
}
if(isMoovRead){//moov在mdat的前面,已经下载
//下载mdat到dest
downLoadMadta(bis,mdatSize,raf,headSize - boxSize+ 8,handler);
bis.close();
is.close();
raf.close();
httpConn.disconnect();
return;
}else {//moov在mdat的后面
//下载moov到dest 与mp4服务器端重新连接一个通道,专门下载moov部分
downLoadMoov(url,headSize,raf);
//从destFile的起点开始,下载mdat到destFile
downLoadMadta(bis,mdatSize,raf,headSize - boxSize+ 8,handler);
bis.close();
is.close();
raf.close();
httpConn.disconnect();
return;
}
} else if (boxType.equalsIgnoreCase("free")) {
headSize += boxSize;
} else if (boxType.equalsIgnoreCase("moov")) {
Log.d("chy","moov size:"+boxSize);
headSize += boxSize;
int moovSize = count;
while (moovSize > 0) {
if (moovSize > BUFFER_SIZE) {
readSize = bis.read(buffer);
} else {
readSize = bis.read(buffer, 0, moovSize);
}
if (readSize == -1) break;
raf.write(buffer, 0, readSize);
moovSize -= readSize;
}
isMoovRead = true;
LogUtils.getInstance().i("moov ok");
}
if (isftypRead && isMoovRead && isMdatRead) {
break;
}
boxSize = readBoxSize(bis, boxSizeBuf);
boxType = readBoxType(bis, boxTypeBuf);
LogUtils.getInstance().i("boxSize:"+boxSize+" boxType:"+boxType);
raf.write(boxSizeBuf);
raf.write(boxTypeBuf);
}
}else {
sendMessage(handler, DOWNLOAD_MP4_FAIL, null);
is.close();
bis.close();
raf.close();
}
} catch (Exception e) {
e.printStackTrace();
this.interrupt(); //中断这个分线程
sendMessage(handler, DOWNLOAD_MP4_FAIL, null);
try {
if (is!=null && bis != null && raf != null){
is.close();
bis.close();
raf.close();
}
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
};
thread.start();
// thread = null;
}
/** 此函数只有在确定moov在mdat的后面才可以调用
* @param url:mp4服务器地址
* @param begin:mdat的末点,也就是moov的起点。
* */
private static void downLoadMoov(final String url, int begin, RandomAccessFile raf){
HttpURLConnection httpConn = null;
InputStream is = null;
BufferedInputStream bis = null;
try {
LogUtils.getInstance().i("downLoadMoov");
URL request = new URL(url);
httpConn = (HttpURLConnection) request.openConnection();
httpConn.setConnectTimeout(3000);
httpConn.setDefaultUseCaches(false);
httpConn.setRequestMethod("GET");
httpConn.setRequestProperty("range", "bytes=" + begin + "-");
is = httpConn.getInputStream();
bis = new BufferedInputStream(is);
int readSize =0;
int headSize = 0;// mp4从流里已经读取的长度
byte[] boxSizeBuf = new byte[4];
byte[] boxTypeBuf= new byte[4];
// 由MP4的文件格式读取
int boxSize = readBoxSize(bis, boxSizeBuf);
String boxType = readBoxType(bis, boxTypeBuf);
raf.write(boxSizeBuf);
raf.write(boxTypeBuf);
int count =0;
byte[] buffer = new byte[4*1024];
while(true) {
count = boxSize - 8;
if (boxType.equalsIgnoreCase("free")) {
headSize += boxSize;
} else if (boxType.equalsIgnoreCase("moov")) {
Log.d("chy", "moov size:" + boxSize);
headSize += boxSize;
int moovSize = count;
while (moovSize > 0) {
if (moovSize > BUFFER_SIZE) {
readSize = bis.read(buffer);
} else {
readSize = bis.read(buffer, 0, moovSize);
}
if (readSize == -1) break;
raf.write(buffer, 0, readSize);
moovSize -= readSize;
}
LogUtils.getInstance().i("moov ok");
break;
}
boxSize = readBoxSize(bis, boxSizeBuf);
boxType = readBoxType(bis, boxTypeBuf);
LogUtils.getInstance().i("boxSizeMoov:" + boxSize + " boxTypeMoov:" + boxType);
raf.write(boxSizeBuf);
raf.write(boxTypeBuf);
}
bis.close();
is.close();
httpConn.disconnect();
}catch (Exception e){
try {
bis.close();
is.close();
} catch (IOException e1) {
e1.printStackTrace();
}
if (httpConn!=null)
httpConn.disconnect();
}
}
/**
* 此方法直接定位到mdat的起点开始下载mdata。1、下载部分通知前端 2、下载完成也通知前端
* @param bis:服务器流
* @param mdatSize:mdat的大小
* @param raf:目标文件的句柄
* @param wirteSeek:让目标文件指针跳过ftyp等部分
* */
private static void downLoadMadta(BufferedInputStream bis,int mdatSize,RandomAccessFile raf,long wirteSeek,Handler handler) throws IOException {
LogUtils.getInstance().i("downLoadMadta");
final int buf_size = 56 * 1024;// 56kb
int downloadCount = 0;
boolean viable = false;
byte[] buffer = new byte[BUFFER_SIZE];
int readSize = 0;
if(wirteSeek>0){
raf.seek(wirteSeek);
}
int totalMdatSize = mdatSize;
while (mdatSize > 0) {
readSize = bis.read(buffer);
if(readSize < 0) break;
raf.write(buffer, 0, readSize);
mdatSize -= readSize;
downloadCount += readSize;
if (handler != null && !viable && downloadCount >= buf_size) {
LogUtils.getInstance().i("PLAYER_MP4_MSG");
viable = true;
// 发送开始播放视频消息,通知前台可以播放视频了
sendMessage(handler, PLAYER_MP4_MSG, null);
}
if (viable){
sendMessage(handler,DOWNLOAD_MP4_LOADING_PROCESS,1000L*downloadCount/totalMdatSize);
}
}
LogUtils.getInstance().i("下载完成");
// 发送下载消息 通知前台已经下载完成
if (handler != null) {
sendMessage(handler, DOWNLOAD_MP4_COMPLETION, null);
handler.removeMessages(PLAYER_MP4_MSG);
handler.removeMessages(DOWNLOAD_MP4_COMPLETION);
}
}
/**
* 发送下载消息
*
* @param handler
* @param what
* @param obj
*/
private static void sendMessage(Handler handler, int what, Object obj) {
if (handler != null) {
Message msg = new Message();
msg.what = what;
msg.obj = obj;
handler.sendMessage(msg);
}
}
/**
* 跳转
*
* @param is
* @param count 跳转长度
* @throws IOException
*/
private static void skip(BufferedInputStream is, long count) throws IOException {
while (count > 0) {
long amt = is.skip(count);
if (amt == -1) {
throw new RuntimeException("inputStream skip exception");
}
count -= amt;
}
}
/**
* 读取mp4文件box大小
*
* @param is
* @param buffer
* @return
*/
private static int readBoxSize(InputStream is, byte[] buffer) {
int sz = fill(is, buffer);
if (sz == -1) {
return 0;
}
return bytesToInt(buffer, 0, 4);
}
/**
* 读取MP4文件box类型
*
* @param is
* @param buffer
* @return
*/
private static String readBoxType(InputStream is, byte[] buffer) {
fill(is, buffer);
return byteToString(buffer);
}
/**
* byte转换int
*
* @param buffer
* @param pos
* @param bytes
* @return
*/
private static int bytesToInt(byte[] buffer, int pos, int bytes) {
/*
* int intvalue = (buffer[pos + 0] & 0xFF) << 24 | (buffer[pos + 1] &
* 0xFF) << 16 | (buffer[pos + 2] & 0xFF) << 8 | buffer[pos + 3] & 0xFF;
*/
int retval = 0;
for (int i = 0; i < bytes; ++i) {
retval |= (buffer[pos + i] & 0xFF) << (8 * (bytes - i - 1));
}
return retval;
}
/**
* byte数据转换String
*
* @param buffer
* @return
*/
private static String byteToString(byte[] buffer) {
assert buffer.length == 4;
String retval = new String();
try {
retval = new String(buffer, 0, buffer.length, "ascii");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return retval;
}
private static int fill(InputStream stream, byte[] buffer) {
return fill(stream, 0, buffer.length, buffer);
}
/**
* 读取流数据
*
* @param stream
* @param pos
* @param len
* @param buffer
* @return
*/
private static int fill(InputStream stream, int pos, int len, byte[] buffer) {
int readSize = 0;
try {
readSize = stream.read(buffer, pos, len);
if (readSize == -1) {
return -1;
}
assert readSize == len : String.format("len %d readSize %d", len,
readSize);
} catch (IOException e) {
e.printStackTrace();
}
return readSize;
}
}
上述代码让moov无论是在mdat前面还是在moov的后面都可以播放。
作者:陈宇、茆文涛