use libffmpeg to parse video frame and vid information
This commit is contained in:
parent
fd5542c528
commit
800a48c610
@ -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
|
||||
|
@ -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
|
||||
|
@ -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=
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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))
|
||||
|
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
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
Package: OpenMediaCenter
|
||||
Depends: nginx, mariadb-server, mediainfo
|
||||
Depends: nginx, mariadb-server, libffmpeg-ocaml
|
||||
Section: web
|
||||
Priority: optional
|
||||
Architecture: all
|
||||
|
Loading…
Reference in New Issue
Block a user