鸿蒙OS短视频开发--边下边播实现
下载工具Mp4DownloadUtils
参考文章:Android 因moov播放网络mp4失败的解决办法_maowentao0416的博客-CSDN博客
主要是让moov移到前面,实现边下边播。短视频刷视频快速出现就是用了边下边播的原理。
管理播放器地址:VideoPlayerView https://gitee.com/ts_ohos/VideoPlayerManager.git
import com.mytoutou.video.manage.player.ui.VideoPlayerView;import com.mytoutou.video.provider.PlayInfo;import com.thin.downloadmanager.util.Log;import ohos.app.Context;import ohos.eventhandler.EventHandler;import ohos.eventhandler.InnerEvent;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;import java.util.concurrent.ConcurrentHashMap;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;/** * 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; /** * 线程池去管理线程 */ private final static ExecutorService service = Executors.newFixedThreadPool(8); public static ConcurrentHashMap consoleThreadMap = new ConcurrentHashMap(); //创建hashmap,用于存储线程 /** * 下载MP4文件 * * @param url * @param fileName * @param handler * @return */ public static File downloadMp4File(final String url, final String fileName, final EventHandler handler, Context context, VideoPlayerView view) { final String path = context.getExternalCacheDir().getPath() + "/" + fileName; final File mp4File = new File(path); downloadVideoToFile(url, mp4File, view, 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, VideoPlayerView view, final EventHandler 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"); httpConn.connect(); 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; Log.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(dstFile, view, 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(dstFile, view, 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; Log.i("moov ok");}if (isftypRead && isMoovRead && isMdatRead) { break;}boxSize = readBoxSize(bis, boxSizeBuf);boxType = readBoxType(bis, boxTypeBuf);Log.i("boxSize:" + boxSize + " boxType:" + boxType);raf.write(boxSizeBuf);raf.write(boxTypeBuf); } } else { PlayInfo playInfo = new PlayInfo(); playInfo.setUrl(dstFile.getPath()); playInfo.setView(view); sendMessage(handler, DOWNLOAD_MP4_FAIL, playInfo); 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;// service.submit(thread);// consoleThreadMap.put(url,thread); } /** * 此函数只有在确定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 { Log.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; } Log.i("moov ok"); break; } boxSize = readBoxSize(bis, boxSizeBuf); boxType = readBoxType(bis, boxTypeBuf); Log.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(File dstFile, VideoPlayerView view, BufferedInputStream bis, int mdatSize, RandomAccessFile raf, long wirteSeek, EventHandler handler) throws IOException { Log.i("downLoadMadta");// final int buf_size = 56 * 1024;// 56kb final int buf_size = 128 * 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 = buf_size) { Log.i("PLAYER_MP4_MSG"); viable = true; // 发送开始播放视频消息,通知前台可以播放视频了 PlayInfo playInfo = new PlayInfo(); playInfo.setUrl(dstFile.getPath()); playInfo.setView(view); sendMessage(handler, PLAYER_MP4_MSG, playInfo); } if (viable) { sendMessage(handler, DOWNLOAD_MP4_LOADING_PROCESS, 1000L * downloadCount / totalMdatSize); } } Log.i("下载完成"); // 发送下载消息 通知前台已经下载完成 if (handler != null) { sendMessage(handler, DOWNLOAD_MP4_COMPLETION, null); handler.removeEvent(PLAYER_MP4_MSG); handler.removeEvent(DOWNLOAD_MP4_COMPLETION); } } /** * 发送下载消息 * * @param handler * @param what * @param obj */ private static void sendMessage(EventHandler handler, int what, Object obj) { if (handler != null) { InnerEvent event = InnerEvent.get(what, obj); handler.sendEvent(event); } } /** * 跳转 * * @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; }}
/** * 线程通知播放所需要的信息 */public class PlayInfo { private String url; private VideoPlayerView view; public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public VideoPlayerView getView() { return view; } public void setView(VideoPlayerView view) { this.view = view; }}
超强干货来袭 云风专访:近40年码龄,通宵达旦的技术人生