use libffmpeg to parse video frame and vid information
This commit is contained in:
22
apiGo/videoparser/thumbnail/Thumbnailparser.go
Normal file
22
apiGo/videoparser/thumbnail/Thumbnailparser.go
Normal file
@ -0,0 +1,22 @@
|
||||
package thumbnail
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type VidInfo struct {
|
||||
Width uint32
|
||||
Height uint32
|
||||
Length uint64
|
||||
FrameRate float32
|
||||
Size int64
|
||||
}
|
||||
|
||||
func EncodeBase64(data *[]byte, mimetype string) *string {
|
||||
strEncPic := base64.StdEncoding.EncodeToString(*data)
|
||||
backpic64 := fmt.Sprintf("data:%s;base64,%s", mimetype, strEncPic)
|
||||
|
||||
return &backpic64
|
||||
|
||||
}
|
183
apiGo/videoparser/thumbnail/Thumbnailparser_shared_ffmpeg.go
Normal file
183
apiGo/videoparser/thumbnail/Thumbnailparser_shared_ffmpeg.go
Normal file
@ -0,0 +1,183 @@
|
||||
// +build sharedffmpeg
|
||||
|
||||
package thumbnail
|
||||
|
||||
import (
|
||||
"github.com/3d0c/gmf"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
func Parse(filename string, time uint64) (*string, *VidInfo, error) {
|
||||
dta, inf, err := decodePic(filename, "png", time)
|
||||
if len(dta) > 0 && err == nil {
|
||||
// base64 encode picture
|
||||
enc := EncodeBase64(dta, "image/png")
|
||||
return enc, inf, nil
|
||||
} else {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func decodePic(srcFileName string, decodeExtension string, time uint64) (pic *[]byte, info *VidInfo, err error) {
|
||||
var swsctx *gmf.SwsCtx
|
||||
|
||||
stat, err := os.Stat(srcFileName)
|
||||
if err != nil {
|
||||
// file seems to not even exist
|
||||
return nil, nil, err
|
||||
}
|
||||
fileSize := stat.Size()
|
||||
|
||||
inputCtx, err := gmf.NewInputCtx(srcFileName)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Error creating context - %s\n", err)
|
||||
return nil, nil, err
|
||||
}
|
||||
defer inputCtx.Free()
|
||||
|
||||
srcVideoStream, err := inputCtx.GetBestStream(gmf.AVMEDIA_TYPE_VIDEO)
|
||||
if err != nil {
|
||||
log.Printf("No video stream found in '%s'\n", srcFileName)
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
codec, err := gmf.FindEncoder(decodeExtension)
|
||||
if err != nil {
|
||||
log.Printf("%s\n", err)
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
cc := gmf.NewCodecCtx(codec)
|
||||
defer gmf.Release(cc)
|
||||
|
||||
cc.SetTimeBase(gmf.AVR{Num: 1, Den: 1})
|
||||
|
||||
cc.SetPixFmt(gmf.AV_PIX_FMT_RGBA).SetWidth(srcVideoStream.CodecPar().Width()).SetHeight(srcVideoStream.CodecPar().Height())
|
||||
if codec.IsExperimental() {
|
||||
cc.SetStrictCompliance(gmf.FF_COMPLIANCE_EXPERIMENTAL)
|
||||
}
|
||||
|
||||
if err := cc.Open(nil); err != nil {
|
||||
log.Println(err)
|
||||
return nil, nil, err
|
||||
}
|
||||
defer cc.Free()
|
||||
|
||||
ist, err := inputCtx.GetStream(srcVideoStream.Index())
|
||||
if err != nil {
|
||||
log.Printf("Error getting stream - %s\n", err)
|
||||
return nil, nil, err
|
||||
}
|
||||
defer ist.Free()
|
||||
|
||||
err = inputCtx.SeekFrameAt(int64(time), 0)
|
||||
if err != nil {
|
||||
log.Printf("Error while seeking file: %s\n", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// convert source pix_fmt into AV_PIX_FMT_RGBA
|
||||
// which is set up by codec context above
|
||||
icc := srcVideoStream.CodecCtx()
|
||||
if swsctx, err = gmf.NewSwsCtx(icc.Width(), icc.Height(), icc.PixFmt(), cc.Width(), cc.Height(), cc.PixFmt(), gmf.SWS_BICUBIC); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer swsctx.Free()
|
||||
|
||||
frameRate := float32(ist.GetRFrameRate().AVR().Num) / float32(ist.GetRFrameRate().AVR().Den)
|
||||
inf := VidInfo{
|
||||
Width: uint32(icc.Width()),
|
||||
Height: uint32(icc.Height()),
|
||||
FrameRate: frameRate,
|
||||
Length: uint64(inputCtx.Duration()),
|
||||
Size: fileSize,
|
||||
}
|
||||
|
||||
info = &inf
|
||||
|
||||
var (
|
||||
pkt *gmf.Packet
|
||||
frames []*gmf.Frame
|
||||
drain int = -1
|
||||
frameCount int = 0
|
||||
)
|
||||
|
||||
for {
|
||||
if drain >= 0 {
|
||||
break
|
||||
}
|
||||
|
||||
pkt, err = inputCtx.GetNextPacket()
|
||||
if err != nil && err != io.EOF {
|
||||
if pkt != nil {
|
||||
pkt.Free()
|
||||
}
|
||||
log.Printf("error getting next packet - %s", err)
|
||||
break
|
||||
} else if err != nil && pkt == nil {
|
||||
drain = 0
|
||||
}
|
||||
|
||||
if pkt != nil && pkt.StreamIndex() != srcVideoStream.Index() {
|
||||
continue
|
||||
}
|
||||
|
||||
frames, err = ist.CodecCtx().Decode(pkt)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Fatal error during decoding - %s\n", err)
|
||||
break
|
||||
}
|
||||
|
||||
// Decode() method doesn't treat EAGAIN and EOF as errors
|
||||
// it returns empty frames slice instead. Countinue until
|
||||
// input EOF or frames received.
|
||||
if len(frames) == 0 && drain < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if frames, err = gmf.DefaultRescaler(swsctx, frames); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
packets, err := cc.Encode(frames, drain)
|
||||
if len(packets) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
picdata := packets[0].Data()
|
||||
pic = &picdata
|
||||
|
||||
// cleanup here
|
||||
for _, p := range packets {
|
||||
p.Free()
|
||||
}
|
||||
|
||||
for i, _ := range frames {
|
||||
frames[i].Free()
|
||||
frameCount++
|
||||
}
|
||||
|
||||
if pkt != nil {
|
||||
pkt.Free()
|
||||
pkt = nil
|
||||
}
|
||||
|
||||
// we only want to encode first picture then exit
|
||||
break
|
||||
}
|
||||
|
||||
for i := 0; i < inputCtx.StreamsCnt(); i++ {
|
||||
st, _ := inputCtx.GetStream(i)
|
||||
st.CodecCtx().Free()
|
||||
st.Free()
|
||||
}
|
||||
return
|
||||
}
|
130
apiGo/videoparser/thumbnail/Thumbnailparser_syscall.go
Normal file
130
apiGo/videoparser/thumbnail/Thumbnailparser_syscall.go
Normal file
@ -0,0 +1,130 @@
|
||||
// +build !sharedffmpeg
|
||||
|
||||
package thumbnail
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type ExtDependencySupport struct {
|
||||
FFMpeg bool
|
||||
MediaInfo bool
|
||||
}
|
||||
|
||||
func Parse(filename string, time uint64) (*string, *VidInfo, error) {
|
||||
// check if the extern dependencies are available
|
||||
mExtDepsAvailable := checkExtDependencySupport()
|
||||
fmt.Printf("FFMPEG support: %t\n", mExtDepsAvailable.FFMpeg)
|
||||
fmt.Printf("MediaInfo support: %t\n", mExtDepsAvailable.MediaInfo)
|
||||
|
||||
var pic *string = nil
|
||||
var info *VidInfo = nil
|
||||
|
||||
if mExtDepsAvailable.FFMpeg {
|
||||
p, err := parseFFmpegPic(filename, time)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
pic = EncodeBase64(p, "image/jpeg")
|
||||
}
|
||||
|
||||
if mExtDepsAvailable.MediaInfo {
|
||||
i, err := getVideoAttributes(filename)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
info = i
|
||||
}
|
||||
return pic, info, nil
|
||||
}
|
||||
|
||||
// check if a specific system command is available
|
||||
func commandExists(cmd string) bool {
|
||||
_, err := exec.LookPath(cmd)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// ext dependency support check
|
||||
func checkExtDependencySupport() *ExtDependencySupport {
|
||||
var extDepsAvailable ExtDependencySupport
|
||||
|
||||
extDepsAvailable.FFMpeg = commandExists("ffmpeg")
|
||||
extDepsAvailable.MediaInfo = commandExists("mediainfo")
|
||||
|
||||
return &extDepsAvailable
|
||||
}
|
||||
|
||||
func secToString(time uint64) string {
|
||||
return fmt.Sprintf("%02d:%02d:%02d", time/3600, (time/60)%60, time%60)
|
||||
}
|
||||
|
||||
// parse the thumbail picture from video file
|
||||
func parseFFmpegPic(path string, time uint64) (*[]byte, error) {
|
||||
app := "ffmpeg"
|
||||
|
||||
cmd := exec.Command(app,
|
||||
"-hide_banner",
|
||||
"-loglevel", "panic",
|
||||
"-ss", secToString(time),
|
||||
"-i", path,
|
||||
"-vframes", "1",
|
||||
"-q:v", "2",
|
||||
"-f", "singlejpeg",
|
||||
"pipe:1")
|
||||
|
||||
stdout, err := cmd.Output()
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
fmt.Println(string(err.(*exec.ExitError).Stderr))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &stdout, nil
|
||||
}
|
||||
|
||||
func getVideoAttributes(path string) (*VidInfo, error) {
|
||||
app := "mediainfo"
|
||||
|
||||
arg0 := path
|
||||
arg1 := "--Output=JSON"
|
||||
|
||||
cmd := exec.Command(app, arg1, "-f", arg0)
|
||||
stdout, err := cmd.Output()
|
||||
|
||||
var t struct {
|
||||
Media struct {
|
||||
Track []struct {
|
||||
Duration string
|
||||
FileSize string
|
||||
Width string
|
||||
Height string
|
||||
}
|
||||
}
|
||||
}
|
||||
err = json.Unmarshal(stdout, &t)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
duration, err := strconv.ParseFloat(t.Media.Track[0].Duration, 32)
|
||||
filesize, err := strconv.Atoi(t.Media.Track[0].FileSize)
|
||||
width, err := strconv.Atoi(t.Media.Track[1].Width)
|
||||
height, err := strconv.Atoi(t.Media.Track[1].Height)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret := VidInfo{
|
||||
Length: uint64(duration),
|
||||
Size: int64(filesize),
|
||||
Width: uint32(width),
|
||||
Height: uint32(height),
|
||||
FrameRate: 0,
|
||||
}
|
||||
|
||||
return &ret, nil
|
||||
}
|
Reference in New Issue
Block a user