golang之媒体处理
[视频]
获取视频封面图:
1) 如果是使用oss的话, 可以添加指定的后缀生成指定图片
视频截帧: https://help.aliyun.com/zh/oss/user-guide/video-snapshots?spm=a2c4g.11186623.0.0.8ea266d4kR5bST
2) 借助第三方包实现: https://github.com/u2takey/ffmpeg-go
注: 他是需要依赖ffmpeg的
package main import ( "bytes" "fmt" "github.com/disintegration/imaging" ffmpeg "github.com/u2takey/ffmpeg-go" "log" "os" "strings" ) func main() { var videoPath = "./test.mp4" videoPath = "https://hk-cloud-screen-test.oss-cn-hangzhou.aliyuncs.com/ai_moment/108f17a6-2f94-4b2d-8667-d369058a5455.mp4" snapshotName, err := GetSnapshot(videoPath, "test", 1) if err != nil { fmt.Println(snapshotName, err) } } func GetSnapshot(videoPath, snapshotPath string, frameNum int) (snapshotName string, err error) { buf := bytes.NewBuffer(nil) err = ffmpeg.Input(videoPath). Filter("select", ffmpeg.Args{fmt.Sprintf("gte(n,%d)", frameNum)}). Output("pipe:", ffmpeg.KwArgs{"vframes": 1, "format": "image2", "vcodec": "mjpeg"}). WithOutput(buf, os.Stdout). Run() if err != nil { log.Fatal("生成缩略图失败:", err) return "", err } img, err := imaging.Decode(buf) if err != nil { log.Fatal("生成缩略图失败:", err) return "", err } err = imaging.Save(img, snapshotPath+".png") if err != nil { log.Fatal("生成缩略图失败:", err) return "", err } names := strings.Split(snapshotPath, "\\") snapshotName = names[len(names)-1] + ".png" return }
还支持更多操作 转GIF,视频格式转换等操作
获取视频长度:
1)若是oss上的视频,则可以使用阿里云的IMM中的提取视频信息的服务
注意这里获取需要使用到签名之后获取对应的数据
这里使用基于阿里云oss包:
github.com/aliyun/aliyun-oss-go-sdk/oss
示例:
client, err := oss.New("oss-cn-hangzhou.aliyuncs.com", "accessKeyID", "accessKeySecret") if err != nil { panic(err) } bucketName := "buekct名称" bucket, err := client.Bucket(bucketName) if err != nil { panic(err) } objectKey := "pp/cbe1655d-9f67-490c-901a-20932f79443c.mp4" // Get object options := []oss.Option{ oss.AddParam("x-oss-process", "video/info"), } signedURL, err := bucket.SignURL(objectKey, oss.HTTPGet, 10*60, options...) if err != nil { panic(err) } fmt.Println(signedURL) // 获取请求信息 resp, err := http.Get(signedURL) if err != nil { panic(err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) fmt.Println("data:", string(body))
2)对于在线视频
package main import ( "bytes" "encoding/binary" "fmt" "io" "os" ) // 获取本地文件视频秒数 // BoxHeader 信息头 type BoxHeader struct { Size uint32 FourccType [4]byte Size64 uint64 } func main() { url := "./video.mp4" file, err := os.Open(url) if err != nil { panic(err) } duration, err := GetMP4Duration(file) if err != nil { panic(err) } fmt.Println(duration) } // GetMP4Duration 获取视频时长,以秒计 func GetMP4Duration(reader io.ReaderAt) (lengthOfTime uint32, err error) { var info = make([]byte, 0x10) var boxHeader BoxHeader var offset int64 = 0 // 获取moov结构偏移 for { _, err = reader.ReadAt(info, offset) if err != nil { return } boxHeader = getHeaderBoxInfo(info) fourccType := getFourccType(boxHeader) if fourccType == "moov" { break } // 有一部分mp4 mdat尺寸过大需要特殊处理 if fourccType == "mdat" { if boxHeader.Size == 1 { offset += int64(boxHeader.Size64) continue } } offset += int64(boxHeader.Size) } // 获取moov结构开头一部分 moovStartBytes := make([]byte, 0x100) _, err = reader.ReadAt(moovStartBytes, offset) if err != nil { return } // 定义timeScale与Duration偏移 timeScaleOffset := 0x1C durationOffest := 0x20 timeScale := binary.BigEndian.Uint32(moovStartBytes[timeScaleOffset : timeScaleOffset+4]) Duration := binary.BigEndian.Uint32(moovStartBytes[durationOffest : durationOffest+4]) lengthOfTime = Duration / timeScale return } // getHeaderBoxInfo 获取头信息 func getHeaderBoxInfo(data []byte) (boxHeader BoxHeader) { buf := bytes.NewBuffer(data) binary.Read(buf, binary.BigEndian, &boxHeader) return } // getFourccType 获取信息头类型 func getFourccType(boxHeader BoxHeader) (fourccType string) { fourccType = string(boxHeader.FourccType[:]) return }
另外还有其他第三方库可以使用:
go get -u github.com/Stitch-Zhang/gmp4
可以处理非下载模式的在线资源
可以借助ffmpeg, 添加到环境变量中
https://github.com/BtbN/FFmpeg-Builds
package main import ( "context" "fmt" "gopkg.in/vansante/go-ffprobe.v2" "log" "time" ) func main() { api := "https://www.runoob.com/try/demo_source/movie.mp4" //api := "https://hk-ai-test.oss-cn-hangzhou.aliyuncs.com/volcVideo/ac6f7816-d0de-4ddd-b57d-590cc5e2e94f_0.mp4" ctx, cancelFunc := context.WithTimeout(context.Background(), 20*time.Second) defer cancelFunc() data, err := ffprobe.ProbeURL(ctx, api) if err != nil { panic(err) } if data == nil { log.Fatal("获取数据为空") } fmt.Println(data.Format.Duration().Seconds()) }
视频上传oss:
videoUrl := "https://muse.console.volcengine.com/api/storage/objects/media/7307071523758653478_origin.mp4?x-muse-token=ChUIARABGIzen6sGIIyBpasGKgM2MTMSmQEKDgiggI3e06eehGQQl5UBEgAaAhgCIggKAmxmEgJjbioGCNo0EIYDMikKJ3siYml6X2lkIjoiMTAwMDAwMDAxIiwiZGVyaXZlZF91aWQiOiIifUIUChJpY2FyY2guaWFtLmF1dGhycGNKLi9hcGkvc3RvcmFnZS9vYmplY3RzL21lZGlhLzczMDcwNzE1MjM3NTg2NTM0Nzga2AJpSExFbFAzM0RjWFZLSzJDenk5UXVlU3QxZXRidlkwSDZnaFBDdFB2VkZ4YjU3YjFnVlpmdlBkaEFtK3pZeEx1U3N2aWEydmZ1RVBaRStFNmd0aFdJSk5sUmFVM3RvNDkvWTZ1ME1IWUlOeVY5bjdicWxCZFI3ZGhZT2VRd3dZS2dXSEdhTzl5VllxSVRQcTgvdk14M3lXd1hvQ21LWjYwY0tUN2dBMlFjQStIN0cxWnJwRzc0bzU2UEtqd3ZTbVVPYzBLdlhtN1pJVTROOStTVFVoaExZdGpMUzFWVEo2Z3FqalVZeGMxV0FsTkN0WXJ6YitabitJandHMTl1WGMveHJZb2xOeFhZQk9sZ3JwNXBQTmV5QmRBYVdQSHpPdDlTbHpITytpeXJITzR3SmRQc0FWQ1E0cXJPbnpPaGJlWEdTVENZME5KelJISnJzT0ZuZE4xTFE9PQ==&infer_mime=ext" resp, err := http.Get(videoUrl) if err != nil { fmt.Println("网络发生错误") return } if resp.StatusCode == http.StatusNotFound { fmt.Println("图片资源不存在") return } defer resp.Body.Close() fmt.Println(resp.StatusCode) // 读取文件内容 body, err := io.ReadAll(resp.Body) // 读取本地文件并上传 //videoPath := "./video.mp4" //f, err := os.Open(videoPath) //if err != nil { // panic(err) //} //fmt.Println("-----读取视频文件") //defer f.Close() //data := make([]byte, 0) //n, err := f.Read(data) //fmt.Println(n, err) // err = bucket.PutObject("demo.mp4", bytes.NewReader(body)) fmt.Println("上传结果:", err)
[图片]
上传图片到oss:
//imgPath := "./oss.jpg" // 本地读取文件后上传 //f, err := os.OpenFile(imgPath, os.O_RDONLY, 0644) //if err != nil { // panic(err) //} //defer f.Close() // //stat, err := f.Stat() //if err != nil { // panic(err) //} // //// 创建一个byte切片 //data := make([]byte, stat.Size()) //count, err := f.Read(data) //fmt.Println("文件长度:", count) // 读取一个文件base64 pictureUrl := "http://pic.netbian.com/uploads/allimg/210423/224716-1619189236e4d9.jpg" //pictureUrl := "http://pic.netbian.com/uploads/allimg/210423/224716-16191892369.jpg" // 不存在的图片 resp, err := http.Get(pictureUrl) if err != nil { fmt.Println("网络发生错误") return } if resp.StatusCode == http.StatusNotFound { fmt.Println("图片资源不存在") return } defer resp.Body.Close() fmt.Println(resp.StatusCode) // 读取文件内容 body, err := io.ReadAll(resp.Body) // 将[]byte数据转换成base64的字符串 imgBase64 := base64.StdEncoding.EncodeToString(body) fmt.Println("img base64:", len(imgBase64)) // 解析base64字符串到[]byte decode, err := base64.StdEncoding.DecodeString(imgBase64) err = bucket.PutObject("test222.jpg", bytes.NewReader(decode)) // 从本地文件上传 //err = bucket.PutObjectFromFile("test.jpg", imgPath) if err != nil { // HandleError(err) panic(err) } fmt.Println("上传图片成功")
获取远程图片信息方法:
func GetImageInfo(imgUrl string) (*types.ImgInfo, error) { resp, err := http.Get(imgUrl) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { logx.Infof("[获取图片信息]失败:%v 状态码", err, resp.StatusCode) return nil, errors.New("获取远程图片失败") } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } resp.Body = io.NopCloser(bytes.NewReader(body)) m, _, err := image.Decode(resp.Body) if err != nil { return nil, err } imgBase64 := base64.StdEncoding.EncodeToString(body) return &types.ImgInfo{ Width: m.Bounds().Dx(), Heigh: m.Bounds().Dy(), Base64Str: imgBase64, }, nil }
注:
1. 对于上传到oss的资源,如果使用未绑定域名方式的话, 他会默认附件下载的方式, 如果绑定了域名之后,可以在线查看资源