Android 因moov播放网络mp4失败的解决办法

本文介绍了一种MP4流媒体的分段下载方法,实现了无论moov位于mdat前后的视频播放。通过读取关键字ftyp、free、mdat、moov定位文件结构,并分阶段下载,最终实现边下边播。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

解决思路:
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的后面都可以播放。
作者:陈宇、茆文涛

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值