弱网下视频分片上传:断点续传、排队上传、失败重试、云端溯源绑定

📅 2026/7/12 0:36:31
弱网下视频分片上传:断点续传、排队上传、失败重试、云端溯源绑定
弱网下视频分片上传断点续传、排队上传、失败重试、云端溯源绑定大家好我是黒漂技术佬。视频专题第二篇也是系列的倒数第二篇咱们聊视频上传。录好的视频存在本地没用出问题的时候得能调出来看。但4G弱网环境下传一个几MB的视频都可能断好几次传一半断了重传又浪费流量。今天给大家一套工业级的视频上传方案断点续传、排队调度、优先级管理、云端绑定全套都有。一、为什么视频上传是个难题先说说4G环境下传视频的痛点上传的几大难题网络不稳定传一半断了重传又得从头来浪费流量带宽有限4G上传带宽本来就小视频传起来慢不能影响业务上传占满带宽开门指令延迟了就麻烦了流量成本视频文件不小传多了流量费扛不住要能溯源传上去的视频得能跟订单、事件对应上核心目标稳定传上去不影响业务尽量省流量传完能对应上。二、分片上传与断点续传这是大文件上传的标准方案视频上传必须上。什么是分片上传把一个大视频文件切成一小块一小块的一块一块传原始视频video.mp4 (8MB) ↓ 切片 分片10 ~ 1MB 分片21MB ~ 2MB 分片32MB ~ 3MB ... 分片87MB ~ 8MB ↓ 逐个上传 全部传完后服务端合并成完整文件分片大小怎么选分片大小优点缺点适用场景256KB失败重传损失小请求次数多开销大网络很差的场景512KB均衡-推荐大部分场景1MB请求少效率高失败损失大网络较好的场景售货柜推荐512KB一片兼顾效率和容错。断点续传原理传了一半断了下次接着传不从头来上传前先问服务端这个文件传了多少了服务端返回已上传的分片列表客户端跳过已经传完的从下一片开始传全部传完通知服务端合并客户端实现publicclassChunkUploader{privatestaticfinalintCHUNK_SIZE512*1024;// 512KB一片/** * 上传视频文件 */publicbooleanuploadVideo(StringfilePath,StringfileId){FilefilenewFile(filePath);longfileSizefile.length();Stringmd5FileUtils.getFileMD5(file);// 1. 先查询已上传的分片ListIntegeruploadedChunksqueryUploadedChunks(fileId,md5);// 2. 计算总分片数inttotalChunks(int)Math.ceil((double)fileSize/CHUNK_SIZE);// 3. 逐个上传未完成的分片for(inti0;itotalChunks;i){if(uploadedChunks.contains(i)){continue;// 已经传过了跳过}booleansuccessuploadChunk(file,i,fileId,md5,totalChunks);if(!success){// 这一片传失败了下次重试Log.w(Upload,分片 i 上传失败);returnfalse;}// 每传完一片更新进度updateUploadProgress(fileId,i1,totalChunks);}// 4. 全部传完通知服务端合并returncompleteUpload(fileId,md5,fileSize);}/** * 上传单个分片 */privatebooleanuploadChunk(Filefile,intchunkIndex,StringfileId,Stringmd5,inttotalChunks){// 读取分片数据byte[]chunkDatareadChunk(file,chunkIndex,CHUNK_SIZE);// 构造请求MultipartBody.BuilderbuildernewMultipartBody.Builder().setType(MultipartBody.FORM).addFormDataPart(file_id,fileId).addFormDataPart(chunk_index,String.valueOf(chunkIndex)).addFormDataPart(total_chunks,String.valueOf(totalChunks)).addFormDataPart(file_md5,md5).addFormDataPart(file,chunk_chunkIndex,RequestBody.create(MediaType.parse(video/mp4),chunkData));// 发送请求带超时RequestrequestnewRequest.Builder().url(UPLOAD_URL).post(builder.build()).build();try{Responseresponseclient.newCall(request).execute();returnresponse.isSuccessful();}catch(IOExceptione){returnfalse;}}}MD5校验每个分片和整个文件都要校验防止传错了分片级每片传完校验MD5不对就重传这一片文件级全部传完校验整个文件的MD5确保完整三、上传队列与优先级调度不是所有视频都一样急分优先级排队传。优先级划分优先级视频类型上传时机说明最高异常事件视频立即上传异常开门、告警、人工调取高投诉/纠纷订单视频优先上传用户投诉了后台申请调取中正常开门视频闲时上传普通交易不急低巡检/测试视频有剩余流量再传可有可无的上传队列实现publicclassUploadQueueManager{privatePriorityQueueUploadTaskqueuenewPriorityQueue((a,b)-Integer.compare(b.getPriority(),a.getPriority()));privateUploadTaskcurrentTask;privatebooleanisRunningfalse;/** * 添加上传任务 */publicvoidaddTask(UploadTasktask){// 去重同一个文件不要重复加if(isInQueue(task.getFileId())){return;}queue.offer(task);if(!isRunning){startNextTask();}}/** * 开始下一个任务 */privatesynchronizedvoidstartNextTask(){if(queue.isEmpty()){isRunningfalse;return;}// 先检查网络弱网下不传低优先级的if(!canUploadNow()){isRunningfalse;return;}currentTaskqueue.poll();isRunningtrue;// 异步上传newThread(()-{booleansuccesschunkUploader.uploadVideo(currentTask.getFilePath(),currentTask.getFileId());onTaskFinished(currentTask,success);currentTasknull;startNextTask();}).start();}}网络条件判断弱网的时候不传或者只传高优先级的privatebooleancanUploadNow(){intnetworkLevelNetworkManager.getNetworkLevel();switch(networkLevel){caseNET_LEVEL_GOOD:// 网络好都可以传returntrue;caseNET_LEVEL_NORMAL:// 一般只传中优先级及以上returncurrentTask.getPriority()PRIORITY_MEDIUM;caseNET_LEVEL_WEAK:// 弱网只传高优先级returncurrentTask.getPriority()PRIORITY_HIGH;caseNET_LEVEL_BAD:caseNET_LEVEL_DISCONNECTED:// 很差不传returnfalse;default:returnfalse;}}四、失败重试与流量控制失败重试策略上传失败了怎么办跟之前的重连一样指数退避publicclassUploadRetryPolicy{privatestaticfinallongBASE_DELAY10*1000;// 10秒起步privatestaticfinallongMAX_DELAY10*60*1000;// 最多10分钟privatestaticfinalintMAX_RETRY10;// 最多重试10次privateintretryCount0;publiclonggetNextDelay(){if(retryCountMAX_RETRY){return-1;// 超过最大次数放弃}longdelay(long)(BASE_DELAY*Math.pow(2,retryCount));delayMath.min(delay,MAX_DELAY);retryCount;// 加随机抖动return(long)(delay*(0.8Math.random()*0.4));}publicvoidreset(){retryCount0;}}上传限速上传不能占满带宽不然影响MQTT等核心业务/** * 限速输出流控制上传速度 */publicclassRateLimitOutputStreamextendsOutputStream{privatelongbytesPerSecond;// 每秒最多传多少字节privatelongstartTime;privatelongbytesWritten;Overridepublicvoidwrite(byte[]b,intoff,intlen)throwsIOException{// 计算当前速度超了就sleep一下longelapsedSystem.currentTimeMillis()-startTime;if(elapsed0){longcurrentSpeedbytesWritten*1000/elapsed;if(currentSpeedbytesPerSecond){longsleepTime(bytesWritten*1000/bytesPerSecond)-elapsed;if(sleepTime0){try{Thread.sleep(sleepTime);}catch(InterruptedExceptione){}}}}out.write(b,off,len);bytesWrittenlen;}}限速策略正常业务期间限制在 200KB/s 以内不抢业务带宽闲时凌晨可以放开到 512KB/s 甚至更高弱网下进一步降低或者暂停上传五、闲时上传与流量管控闲时上传策略普通视频不着急留到流量便宜的时候传publicclassIdleTimeUploadScheduler{// 闲时段凌晨2点到6点privatestaticfinalintIDLE_START_HOUR2;privatestaticfinalintIDLE_END_HOUR6;/** * 判断当前是不是闲时 */publicbooleanisIdleTime(){inthourCalendar.getInstance().get(Calendar.HOUR_OF_DAY);returnhourIDLE_START_HOURhourIDLE_END_HOUR;}/** * 调度上传任务 */publicvoidscheduleUpload(UploadTasktask){if(task.getPriority()PRIORITY_HIGH){// 高优先级立刻传uploadQueue.addTask(task);}else{// 低优先级等闲时再传pendingIdleTasks.add(task);}}}月流量配额控制每个月流量有限超了就不传非必要的publicbooleanhasEnoughTrafficForUpload(longfileSize){longmonthUsedTrafficMonitor.getMonthTraffic();longmonthLimitConfigManager.getMonthTrafficLimit();longremainingmonthLimit-monthUsed;// 剩余流量超过文件大小的2倍才传留余量returnremainingfileSize*2;}六、云端溯源绑定视频传上去了得能跟订单、事件对应起来。上传时带上业务信息publicclassVideoUploadInfo{privateStringfileId;// 文件唯一IDprivateStringdeviceNo;// 设备编号privateStringeventType;// 事件类型open/abnormal/shakeprivateStringrelatedOrderId;// 关联订单号privatelongstartTime;// 录像开始时间privateintduration;// 时长privateStringfileMd5;// 文件MD5privatelongfileSize;// 文件大小}云端存储结构视频存储/ ├── 设备编号/ │ ├── 20260708/ │ │ ├── 订单号1.mp4 │ │ ├── 异常事件_时间戳.mp4 │ │ └── ... │ └── 20260709/ │ └── ...按设备日期分目录文件名带订单号或事件信息后台管理系统可以直接按订单号查视频。回调通知传完了通知后台后台把视频和订单绑定privatevoidonUploadSuccess(UploadTasktask){// 更新数据库状态VideoRecordDb.getInstance().updateUploadStatus(task.getFileId(),UPLOAD_STATUS_SUCCESS,System.currentTimeMillis());// 通知后台JSONObjectpayloadnewJSONObject();payload.put(file_id,task.getFileId());payload.put(video_url,task.getRemoteUrl());payload.put(related_order_id,task.getRelatedOrderId());payload.put(event_type,task.getEventType());MqttManager.publish(device/DeviceInfo.getDeviceNo()/video/uploaded,payload.toString(),1);}七、上传状态持久化APP重启了上传进度不能丢。上传任务持久化把待上传的任务存数据库CREATETABLEupload_task(idINTEGERPRIMARYKEYAUTOINCREMENT,file_idVARCHAR(64)UNIQUE,-- 文件IDfile_pathVARCHAR(256),-- 本地路径priorityINTEGER,-- 优先级event_typeVARCHAR(32),-- 事件类型related_idVARCHAR(64),-- 关联IDstatusINTEGERDEFAULT0,-- 0待上传 1上传中 2成功 3失败uploaded_chunksTEXT,-- 已上传分片列表JSONretry_countINTEGERDEFAULT0,-- 重试次数create_timeBIGINT,update_timeBIGINT);启动恢复APP启动时从数据库恢复未完成的任务publicvoidrestorePendingTasks(){ListUploadTaskpendingUploadTaskDb.getInstance().queryPendingTasks();for(UploadTasktask:pending){if(task.getStatus()UPLOAD_STATUS_UPLOADING){// 上次传了一半重置为待上传从断点继续task.setStatus(UPLOAD_STATUS_PENDING);}uploadQueue.addTask(task);}}八、踩坑经验坑1分片传完了但合并失败现象所有分片都显示传成功了但合并后的文件损坏原因某一片传的时候数据错了MD5没校验好解决每片传完服务端都校验MD5不对就让重传合并后再校验整个文件的MD5坑2上传过程中网络切换现象传一半WiFi切4GIP变了连接断了解决分片上传天然支持断点续传断了重新查进度接着传就行坑3视频还在录制就开始传现象文件还在写读出来的数据不完整解决录制完成、文件关闭后再加入上传队列上传前检查文件是否完整坑4大量视频同时上传把带宽占满现象批量恢复上传的时候几十个视频一起传MQTT都连不上了解决单队列串行上传同时只传一个加上传限速弱网下只传高优先级九、总结弱网视频上传的核心分片断点排队限速。分片上传512KB一片失败只重传一片省流量断点续传传了多少都记录重启断网都不怕优先级队列异常、投诉优先普通视频闲时再传上传限速不能抢业务带宽保证开门等核心功能优先流量控制月配额、闲时策略控制成本溯源绑定视频和订单、事件关联方便后台查询这套方案下来4G弱网环境下视频上传也能稳稳的不用再担心传一半断了白忙活。我是黒漂技术佬专注无人零售物联网实战。五大模块的12篇细分文章到这里就全部结束了下一篇是整个系列的收官总结把全链路的核心要点和踩坑经验串起来。有视频上传的坑欢迎私信交流大家一起避坑~