Gin存储文件与oss对象存储(二)

Gin存储文件与oss对象存储(二)

原创 何泽丰 ProgrammerHe
 

Gin存储文件与oss对象存储(二)

概述

朋友们大家好啊,这一篇笔记我们来简单记录一下前端在Vue2项目中base64转图片,在文件上传时实现分片上传、断点续传功能;最后将视频文件存储到OSS对象存储,转码为M3U8格式在Web前端实现HLS播放。

前端base64转图片

图片

后端直接返回base64格式字符串不需要做额外的数据封装,格式转换在客户端进行,按照上一篇笔记的做法,后端将某个路径下的图片取回并转换成base64编码的图像字符串返回给web前端,前端接收后将该转换成图像。

...

this.base64Data= res.data.pic;
this.imageSrc='data:image/png;base64,'+this.base64Data;
this.convertBase64ToImage();
...

convertBase64ToImage(){
const img =newImage();// 创建一个Image对象
    img.src=this.imageSrc;
    img.onload=() =>{
const canvas =document.createElement('canvas');// 创建一个canvas对象
const context = canvas.getContext('2d');
        canvas.width= img.width;// 设置canvas长宽与图像保持一致
        canvas.height= img.height;
        context.drawImage(img,0,0);
const imageURL = canvas.toDataURL('image/png');// 调用canvas的toDataURL方法
this.imageSrc= imageURL;// 更新 imageSrc
    };
},

分片传输与断点续传

分片传输有不少好处,上传时将大文件分割成多个小分片进行上传,能提高文件的上传速度、减少内存消耗。而断点续传可以在传输中断后继续传输,提高数据传输的稳定性和可靠性,避免重复传输整个大文件、节省了时间和带宽。简易实现的思路是前端将大文件分割成同等大小的分片(最后一片除外),调用后端接口将分片上传,上传完成后再告诉后端将分片合成文件。至于最简单的断点续传实现,本次没有在后端做校验,只在前端实现记录上传分片索引实现的断点续传功能。

前端思路与实现

那我们可以将分片传输分成两步,将文件分片再上传。

文件通过input组件上传后,通过handleFileChange函数处理,获取用户选择的第一个文件(event.target.files[0]),event是触发事件的对象,target指的是事件的目标元素。当点击开始上传时调用uploadFile方法,判断文件是否为空或是否已暂停上传。然后根据chunkSize切割分片,分出来之后调用reader.readAsArrayBuffer方法,readAsArrayBuffer会异步执行,将读取文件数据到内存中,完成后会触发onloadend事件上传单个分片,直到所有分片传输完成。

// 放几个按钮
<div class="user-upload-file">
<input type="file"ref="fileInput"@change="handleFileChange"/>
<button @click="uploadFile">开始上传</button>
<button @click="pauseUpload">暂停上传</button>
<button @click="resumeUpload">继续上传</button>
<button @click="cancelUpload">取消上传</button>
</div>

...
<script>
exportdefault{
...
   data(){
return{
            file:null,// 存储选择的文件
              chunkSize:5*1024*1024,// 每片 分片大小设置为5M
              totalChunks:0,// 所有分片总数
              currentChunk:0,// 初始化分片索引
              isPaused:false,// 是否暂停上传
}
},
...
    methods:{
      handleFileChange(event){
this.file =event.target.files[0];
this.totalChunks =Math.ceil(this.file.size /this.chunkSize);// 计算总分片数
this.currentChunk =0;// 当前分片索引是0
this.isPaused =false;// 没有在暂停传输
},
    async uploadFile(){
if(!this.file){
        alert("没有文件上传");
return;
}

while(this.currentChunk <this.totalChunks){
if(this.isPaused){
          console.log("上传已暂停")
return;
}
const start =this.currentChunk *this.chunkSize;
constend=Math.min(start +this.chunkSize,this.file.size);
const chunk =this.file.slice(start,end);

        await newPromise((resolve, reject)=>{
const reader =newFileReader();
          reader.onloadend =()=>{
// 上传分片
this.uploadChunk(reader.result,this.currentChunk,this.totalChunks)
.then(()=>{
                  resolve();// 分片上传成功后继续处理下一个分片
this.currentChunk ++;
})
.catch((err)=>{
                  reject(err);// 上传失败,停止并返回错误
});
};
// 读取分片数据
          reader.readAsArrayBuffer(chunk);
    });
}
}
}
</script>

在全部分片传输完成后调一下后端的接口,表示可以开始将文件合并了。

... 
asyncmergeChunk(filename, totalChunk){
console.log("mergeChunk=>", filename, totalChunk);
try{
const formData =newFormData();
        formData.append('fileName', filename);
        formData.append('totalChunk', totalChunk);

// 发送请求
const response =await axios.post('/users/merge_chunk', formData,{
headers:{
"Content-Type":"'multipart/form-data",
}
});
console.log(response);
}catch(err){
console.log(err);
}
},

// 暂停上传
pauseUpload(){
this.isPaused=true;
console.log("暂停上传");
},


// 继续上传
resumeUpload(){
if(this.isPaused){
this.isPaused=false;
console.log("继续上传");
this.uploadFile();// 继续上传
    }
},

后端思路与实现

  1. 接收上传分片;将分片先暂存到某个文件夹,并根据上传的index给分片命名。
// 处理上传分片的接口
func (*UserHandler)HandleUploadChunk(ctx *gin.Context){
...
// 存放临时文件夹
const chunkDir ="C:\\Users\\hzf19\\Desktop\\chunk"

// 检查分片存储目录是否存在,不存在则创建
if _, err := os.Stat(chunkDir); os.IsNotExist(err){
        err := os.MkdirAll(chunkDir, os.ModePerm)// 创建目录
if err !=nil{
            log.Println("创建分片目录失败:", err)
            ctx.JSON(500, gin.H{"error":"创建分片目录失败"})
return
}
}

    file, err := ctx.FormFile("chunk")
if err !=nil{
        log.Println(err)
        ctx.JSON(400, gin.H{"error":"文件上传失败"})
return
}

// 获取文件名,文件名从表单字段获取
    fileName := ctx.DefaultPostForm("fileName","")// 获取文件名
if fileName ==""{
        fileName = file.Filename// 如果没有从表单中获取到文件名,则使用上传的文件名
}
    index, err := strconv.Atoi(ctx.DefaultPostForm("index","0"))// 分片索引
if err !=nil{
        log.Println(err,"无效的分片索引")
        ctx.JSON(400, gin.H{"error":"无效的分片索引"})
return
}
    chunkCount, err := strconv.Atoi(ctx.DefaultPostForm("chunkCount","0"))// 获取总分片数
if err !=nil{
        ctx.JSON(400, gin.H{"error":"无效的总分片数"})
return
}

// 创建存储分片的临时文件
    chunkFilePath := filepath.Join(chunkDir, fmt.Sprintf("%s-chunk-%d", fileName, index))
// 保存分片到临时文件
if err := ctx.SaveUploadedFile(file, chunkFilePath); err !=nil{
        ctx.JSON(500, gin.H{"error":"保存分片文件失败"})
return
}

// 返回上传成功的响应
    ctx.JSON(200, gin.H{
"message":"分片上传成功",
"chunkIndex": index,
"chunkCount": chunkCount,
})
}
  1. 合并分片接口。就说合并时要顺序读取分片,不然合并后文件就损坏了。
HandleMergeChunk(ctx *gin.Context){
....
    fileName := ctx.PostForm("fileName")
    totalChunk, err := strconv.Atoi(ctx.PostForm("totalChunk"))
if err !=nil|| totalChunk <=0{
        ctx.JSON(400, gin.H{"error":"无效的总分片数"})
return
}

// 分片的临时文件夹
const chunkDir ="C:\\Users\\hzf19\\Desktop\\chunk"

// 文件存储路径
    mergePath := fmt.Sprintf("C:\\Users\\hzf19\\Desktop\\1821841651400708096\\%s", fileName)

// 确保临时文件夹存在
if _, err := os.Stat(chunkDir); os.IsNotExist(err){
        ctx.JSON(500, gin.H{"error":"临时文件不存在"})
return
}

// 创建合并文件
    mergeFile, err := os.Create(mergePath)
if err !=nil{
        ctx.JSON(500, gin.H{"error":"创建合并文件失败"})
return
}
defer mergeFile.Close()

// 按索引顺序合并分片 顺序很重要
for i :=0; i < totalChunk; i++{
        chunkPath := filepath.Join(chunkDir, fmt.Sprintf("%s-chunk-%d", fileName, i))
        chunkData, err := os.ReadFile(chunkPath)
if err !=nil{
            log.Printf("读取分片失败: %v", err)
            ctx.JSON(500, gin.H{"error": fmt.Sprintf("读取分片 %d 失败: %v", i, err)})
return
}
if _, err := mergeFile.Write(chunkData); err !=nil{
            log.Printf("写入分片失败: %v", err)
            ctx.JSON(500, gin.H{"error": fmt.Sprintf("写入分片 %d 失败: %v", i, err)})
return
}
}

    ctx.JSON(200, gin.H{
"data":"merge chunk success",
})
}
 
 
 
 
 

OSS视频mp4转m3u8

m3u8是一种基于HTTP Live Streaming(HLS)协议的媒体播放列表文件格式,主要用于流媒体传输。HLS由苹果公司开发,目前已成为一种广泛支持的标准;HLS通常以UTF-8编码,包含了一系列媒体文件的URL地址,指向视频或音频流的不同分段。m3u8文件实质上是一个播放列表(playlist),它并不包含音视频数据,而是记录了一个索引,播放软件根据这个索引找到对应的音视频文件的地址进行播放。m3u8文件支持自适应码率流、动态更新和分段传输,能够支持稳定、高效的流媒体播放。它通过定义媒体段、播放顺序以及其他元数据,确保了流媒体的可靠传输与播放。与MP4文件相比,HLS将视频分割成许多片段(如5s的 .ts文件),通过M3U8文件中的URL进行有序传输。

这里记的是将视频上传到七牛云OSS,将mp4文件转为m3u8格式,在前端播放器进行播放。

上传到OSS

上传到七牛云oss后,对视频进行转码。转码后将播放链接复制到videoSrc上。

图片图片

前端播放器

 
 
 
 
 

首先需要引用hls的包并引用

npm install hls.js
...
import Hls from 'hls.js';
<div id="app">
<divclass="hls-player">
<video
ref="videoPlayer"
controls
autoplay="true"
style="width:100%; max-width:800px; background:#000;"
></video>
</div>

<script>
exportdefault{
....
    mounted(){
this.initHlsPlayer();
},
    methods:{
    initHlsPlayer(){
const video =this.$refs.videoPlayer;

if(Hls.isSupported()){
const hls =newHls();
        hls.loadSource(this.videoSrc);
        hls.attachMedia(video);

        hls.on(Hls.Events.MANIFEST_PARSED,()=>{
          video.play();
});

        hls.on(Hls.Events.ERROR,(event, data)=>{
          console.error('HLS.js 播放错误:', data);
});
}elseif(video.canPlayType('application/vnd.apple.mpegurl')){
// 如果浏览器原生支持 HLS(如 Safari)
        video.src =this.videoSrc;
        video.addEventListener('loadedmetadata',()=>{
          video.play();
});
}else{
        console.error('该浏览器不支持 HLS 播放');
}
},
}
}

</script>

写在最后

本人是新手小白,如果这篇笔记中有任何错误或不准确之处,真诚地希望各位读者能够给予批评和指正,如有更好的实现方法请给我留言,谢谢!欢迎大家在评论区留言!觉得写得还不错的话欢迎大家关注一波!


对象存储 · 目录
上一篇Gin存储文件与oss对象存储(一)
阅读 277
 
posted @ 2024-12-14 22:08  技术颜良  阅读(6)  评论(0编辑  收藏  举报