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
|
stage: build_backend
|
||||||
script:
|
script:
|
||||||
- cd apiGo
|
- cd apiGo
|
||||||
- go build -v -o openmediacenter
|
- go build -v -tags sharedffmpeg -o openmediacenter
|
||||||
- cp -r ../build/ ./static/
|
- cp -r ../build/ ./static/
|
||||||
- go build -v -tags static -o openmediacenter_full
|
- go build -v -tags static -o openmediacenter_full
|
||||||
- env GOOS=windows GOARCH=amd64 go build -v -tags static -o openmediacenter.exe
|
- env GOOS=windows GOARCH=amd64 go build -v -tags static -o openmediacenter.exe
|
||||||
|
@ -3,6 +3,7 @@ module openmediacenter/apiGo
|
|||||||
go 1.16
|
go 1.16
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/3d0c/gmf v0.0.0-20210830084021-7b27911659a2 // indirect
|
||||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||||
github.com/go-sql-driver/mysql v1.5.0
|
github.com/go-sql-driver/mysql v1.5.0
|
||||||
github.com/pelletier/go-toml/v2 v2.0.0-beta.3
|
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=
|
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 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
||||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
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=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
@ -1,11 +1,7 @@
|
|||||||
package videoparser
|
package videoparser
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"os/exec"
|
|
||||||
"strconv"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func AppendMessage(message string) {
|
func AppendMessage(message string) {
|
||||||
@ -33,88 +29,3 @@ func SendEvent(message string) {
|
|||||||
|
|
||||||
IndexSender.Publish(marshal)
|
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/api/types"
|
||||||
"openmediacenter/apiGo/config"
|
"openmediacenter/apiGo/config"
|
||||||
"openmediacenter/apiGo/database"
|
"openmediacenter/apiGo/database"
|
||||||
|
"openmediacenter/apiGo/videoparser/thumbnail"
|
||||||
"openmediacenter/apiGo/videoparser/tmdb"
|
"openmediacenter/apiGo/videoparser/tmdb"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -13,7 +14,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var mSettings *types.SettingsType
|
var mSettings *types.SettingsType
|
||||||
var mExtDepsAvailable *ExtDependencySupport
|
|
||||||
|
|
||||||
// default Tag ids
|
// default Tag ids
|
||||||
const (
|
const (
|
||||||
@ -22,11 +22,6 @@ const (
|
|||||||
LowQuality = 3
|
LowQuality = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
type ExtDependencySupport struct {
|
|
||||||
FFMpeg bool
|
|
||||||
MediaInfo bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type VideoAttributes struct {
|
type VideoAttributes struct {
|
||||||
Duration float32
|
Duration float32
|
||||||
FileSize uint
|
FileSize uint
|
||||||
@ -35,10 +30,6 @@ type VideoAttributes struct {
|
|||||||
|
|
||||||
func InitDeps(sett *types.SettingsType) {
|
func InitDeps(sett *types.SettingsType) {
|
||||||
mSettings = sett
|
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) {
|
func ReIndexVideos(path []string) {
|
||||||
@ -115,58 +106,50 @@ func addVideo(videoName string, fileName string, year int) {
|
|||||||
var poster *string
|
var poster *string
|
||||||
var tmdbData *tmdb.VideoTMDB
|
var tmdbData *tmdb.VideoTMDB
|
||||||
var err error
|
var err error
|
||||||
|
var insertid int64
|
||||||
// initialize defaults
|
|
||||||
vidAtr := &VideoAttributes{
|
|
||||||
Duration: 0,
|
|
||||||
FileSize: 0,
|
|
||||||
Width: 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
vidFolder := config.GetConfig().General.ReindexPrefix + mSettings.VideoPath
|
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 TMDB grabbing is enabled serach in api for video...
|
||||||
if mSettings.TMDBGrabbing {
|
if mSettings.TMDBGrabbing {
|
||||||
tmdbData = tmdb.SearchVideo(videoName, year)
|
tmdbData = tmdb.SearchVideo(videoName, year)
|
||||||
if tmdbData != nil {
|
if tmdbData != nil {
|
||||||
// reassign parsed pic as poster
|
|
||||||
poster = ppic
|
|
||||||
// and tmdb pic as thumbnail
|
// 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 {
|
if err != nil {
|
||||||
fmt.Printf("Failed to insert video into db: %s\n", err.Error())
|
fmt.Printf("Failed to insert video into db: %s\n", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// add default tags
|
|
||||||
if vidAtr.Width != 0 {
|
|
||||||
insertSizeTag(vidAtr.Width, uint(insertId))
|
|
||||||
}
|
|
||||||
|
|
||||||
// add tmdb tags
|
// add tmdb tags
|
||||||
if mSettings.TMDBGrabbing && tmdbData != nil {
|
if mSettings.TMDBGrabbing && tmdbData != nil {
|
||||||
insertTMDBTags(tmdbData.GenreIds, insertId)
|
insertTMDBTags(tmdbData.GenreIds, insertid)
|
||||||
}
|
}
|
||||||
|
|
||||||
AppendMessage(fmt.Sprintf("%s - added!", videoName))
|
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
|
Package: OpenMediaCenter
|
||||||
Depends: nginx, mariadb-server, mediainfo
|
Depends: nginx, mariadb-server, libffmpeg-ocaml
|
||||||
Section: web
|
Section: web
|
||||||
Priority: optional
|
Priority: optional
|
||||||
Architecture: all
|
Architecture: all
|
||||||
|
Loading…
Reference in New Issue
Block a user