From 800a48c610660fc815636cfcb5adda091858a8c1 Mon Sep 17 00:00:00 2001 From: lukas Date: Sat, 25 Sep 2021 20:49:47 +0200 Subject: [PATCH] use libffmpeg to parse video frame and vid information --- .gitlab-ci.yml | 2 +- apiGo/go.mod | 1 + apiGo/go.sum | 2 + apiGo/videoparser/Helpers.go | 89 --------- apiGo/videoparser/ReIndexVideos.go | 71 +++---- .../videoparser/thumbnail/Thumbnailparser.go | 22 +++ .../Thumbnailparser_shared_ffmpeg.go | 183 ++++++++++++++++++ .../thumbnail/Thumbnailparser_syscall.go | 130 +++++++++++++ deb/OpenMediaCenter/DEBIAN/control | 2 +- 9 files changed, 367 insertions(+), 135 deletions(-) create mode 100644 apiGo/videoparser/thumbnail/Thumbnailparser.go create mode 100644 apiGo/videoparser/thumbnail/Thumbnailparser_shared_ffmpeg.go create mode 100644 apiGo/videoparser/thumbnail/Thumbnailparser_syscall.go diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d16b7e7..1d753b0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -29,7 +29,7 @@ Build_Backend: stage: build_backend script: - cd apiGo - - go build -v -o openmediacenter + - go build -v -tags sharedffmpeg -o openmediacenter - cp -r ../build/ ./static/ - go build -v -tags static -o openmediacenter_full - env GOOS=windows GOARCH=amd64 go build -v -tags static -o openmediacenter.exe diff --git a/apiGo/go.mod b/apiGo/go.mod index bb4b634..1f6b14d 100644 --- a/apiGo/go.mod +++ b/apiGo/go.mod @@ -3,6 +3,7 @@ module openmediacenter/apiGo go 1.16 require ( + github.com/3d0c/gmf v0.0.0-20210830084021-7b27911659a2 // indirect github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/go-sql-driver/mysql v1.5.0 github.com/pelletier/go-toml/v2 v2.0.0-beta.3 diff --git a/apiGo/go.sum b/apiGo/go.sum index 79d3e8e..0d86dc2 100644 --- a/apiGo/go.sum +++ b/apiGo/go.sum @@ -1,4 +1,6 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/3d0c/gmf v0.0.0-20210830084021-7b27911659a2 h1:+wdkl7m6ElGhLyYxHab+uPYf57MQ4XpX6EuG/8kbkLg= +github.com/3d0c/gmf v0.0.0-20210830084021-7b27911659a2/go.mod h1:0QMRcUq2JsDECeAq7bj4h79k7XbhtTsrPUQf6G7qfPs= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/apiGo/videoparser/Helpers.go b/apiGo/videoparser/Helpers.go index 3c82b45..c42cdf5 100644 --- a/apiGo/videoparser/Helpers.go +++ b/apiGo/videoparser/Helpers.go @@ -1,11 +1,7 @@ package videoparser import ( - "encoding/base64" "encoding/json" - "fmt" - "os/exec" - "strconv" ) func AppendMessage(message string) { @@ -33,88 +29,3 @@ func SendEvent(message string) { IndexSender.Publish(marshal) } - -// ext dependency support check -func checkExtDependencySupport() *ExtDependencySupport { - var extDepsAvailable ExtDependencySupport - - extDepsAvailable.FFMpeg = commandExists("ffmpeg") - extDepsAvailable.MediaInfo = commandExists("mediainfo") - - return &extDepsAvailable -} - -// check if a specific system command is available -func commandExists(cmd string) bool { - _, err := exec.LookPath(cmd) - return err == nil -} - -// parse the thumbail picture from video file -func parseFFmpegPic(path string) (*string, error) { - app := "ffmpeg" - - cmd := exec.Command(app, - "-hide_banner", - "-loglevel", "panic", - "-ss", "00:04:00", - "-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 - } - - strEncPic := base64.StdEncoding.EncodeToString(stdout) - if strEncPic == "" { - return nil, nil - } - backpic64 := fmt.Sprintf("data:image/jpeg;base64,%s", strEncPic) - - return &backpic64, nil -} - -func getVideoAttributes(path string) *VideoAttributes { - 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 - } - } - } - err = json.Unmarshal(stdout, &t) - - if err != nil { - fmt.Println(err.Error()) - return nil - } - - 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) - - ret := VideoAttributes{ - Duration: float32(duration), - FileSize: uint(filesize), - Width: uint(width), - } - - return &ret -} diff --git a/apiGo/videoparser/ReIndexVideos.go b/apiGo/videoparser/ReIndexVideos.go index 1a59fa2..1c6459b 100644 --- a/apiGo/videoparser/ReIndexVideos.go +++ b/apiGo/videoparser/ReIndexVideos.go @@ -6,6 +6,7 @@ import ( "openmediacenter/apiGo/api/types" "openmediacenter/apiGo/config" "openmediacenter/apiGo/database" + "openmediacenter/apiGo/videoparser/thumbnail" "openmediacenter/apiGo/videoparser/tmdb" "regexp" "strconv" @@ -13,7 +14,6 @@ import ( ) var mSettings *types.SettingsType -var mExtDepsAvailable *ExtDependencySupport // default Tag ids const ( @@ -22,11 +22,6 @@ const ( LowQuality = 3 ) -type ExtDependencySupport struct { - FFMpeg bool - MediaInfo bool -} - type VideoAttributes struct { Duration float32 FileSize uint @@ -35,10 +30,6 @@ type VideoAttributes struct { func InitDeps(sett *types.SettingsType) { mSettings = sett - // 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) } func ReIndexVideos(path []string) { @@ -115,58 +106,50 @@ func addVideo(videoName string, fileName string, year int) { var poster *string var tmdbData *tmdb.VideoTMDB var err error - - // initialize defaults - vidAtr := &VideoAttributes{ - Duration: 0, - FileSize: 0, - Width: 0, - } + var insertid int64 vidFolder := config.GetConfig().General.ReindexPrefix + mSettings.VideoPath - if mExtDepsAvailable.FFMpeg { - ppic, err = parseFFmpegPic(vidFolder + fileName) - if err != nil { - fmt.Printf("FFmpeg error occured: %s\n", err.Error()) - } else { - fmt.Println("successfully extracted thumbnail!!") - } - } - - if mExtDepsAvailable.MediaInfo { - atr := getVideoAttributes(vidFolder + fileName) - if atr != nil { - vidAtr = atr - } - } - // if TMDB grabbing is enabled serach in api for video... if mSettings.TMDBGrabbing { tmdbData = tmdb.SearchVideo(videoName, year) if tmdbData != nil { - // reassign parsed pic as poster - poster = ppic // and tmdb pic as thumbnail - ppic = &tmdbData.Thumbnail + poster = &tmdbData.Thumbnail + } + } + + // parse pic from 4min frame + ppic, vinfo, err := thumbnail.Parse(vidFolder+fileName, 240) + // use parsed pic also for poster pic + if poster == nil { + poster = ppic + } + + if err != nil { + fmt.Printf("FFmpeg error occured: %s\n", err.Error()) + + query := `INSERT INTO videos(movie_name,movie_url) VALUES (?,?)` + err, insertid = database.Insert(query, videoName, fileName) + + } else { + query := `INSERT INTO videos(movie_name,movie_url,poster,thumbnail,quality,length) VALUES (?,?,?,?,?,?)` + err, insertid = database.Insert(query, videoName, fileName, ppic, poster, vinfo.Width, vinfo.Length) + + // add default tags + if vinfo.Width != 0 && err == nil { + insertSizeTag(uint(vinfo.Width), uint(insertid)) } } - query := `INSERT INTO videos(movie_name,movie_url,poster,thumbnail,quality,length) VALUES (?,?,?,?,?,?)` - err, insertId := database.Insert(query, videoName, fileName, poster, ppic, vidAtr.Width, vidAtr.Duration) if err != nil { fmt.Printf("Failed to insert video into db: %s\n", err.Error()) return } - // add default tags - if vidAtr.Width != 0 { - insertSizeTag(vidAtr.Width, uint(insertId)) - } - // add tmdb tags if mSettings.TMDBGrabbing && tmdbData != nil { - insertTMDBTags(tmdbData.GenreIds, insertId) + insertTMDBTags(tmdbData.GenreIds, insertid) } AppendMessage(fmt.Sprintf("%s - added!", videoName)) diff --git a/apiGo/videoparser/thumbnail/Thumbnailparser.go b/apiGo/videoparser/thumbnail/Thumbnailparser.go new file mode 100644 index 0000000..28179a6 --- /dev/null +++ b/apiGo/videoparser/thumbnail/Thumbnailparser.go @@ -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 + +} diff --git a/apiGo/videoparser/thumbnail/Thumbnailparser_shared_ffmpeg.go b/apiGo/videoparser/thumbnail/Thumbnailparser_shared_ffmpeg.go new file mode 100644 index 0000000..56f57a5 --- /dev/null +++ b/apiGo/videoparser/thumbnail/Thumbnailparser_shared_ffmpeg.go @@ -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 +} diff --git a/apiGo/videoparser/thumbnail/Thumbnailparser_syscall.go b/apiGo/videoparser/thumbnail/Thumbnailparser_syscall.go new file mode 100644 index 0000000..d675396 --- /dev/null +++ b/apiGo/videoparser/thumbnail/Thumbnailparser_syscall.go @@ -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 +} diff --git a/deb/OpenMediaCenter/DEBIAN/control b/deb/OpenMediaCenter/DEBIAN/control index 975ffd8..92dc83a 100755 --- a/deb/OpenMediaCenter/DEBIAN/control +++ b/deb/OpenMediaCenter/DEBIAN/control @@ -1,5 +1,5 @@ Package: OpenMediaCenter -Depends: nginx, mariadb-server, mediainfo +Depends: nginx, mariadb-server, libffmpeg-ocaml Section: web Priority: optional Architecture: all