1
Fork 0

Encode videos for web using ffmpeg

This commit is contained in:
viktorstrate 2020-07-11 13:13:31 +02:00
parent 0ab6048151
commit 26a5c5ac29
7 changed files with 90 additions and 25 deletions

View File

@ -33,5 +33,6 @@ require (
golang.org/x/image v0.0.0-20200618115811-c13761719519 golang.org/x/image v0.0.0-20200618115811-c13761719519
golang.org/x/mod v0.3.0 // indirect golang.org/x/mod v0.3.0 // indirect
golang.org/x/tools v0.0.0-20200622192924-4fd1c64487bf // indirect golang.org/x/tools v0.0.0-20200622192924-4fd1c64487bf // indirect
gopkg.in/vansante/go-ffprobe.v2 v2.0.2
gopkg.in/yaml.v2 v2.3.0 // indirect gopkg.in/yaml.v2 v2.3.0 // indirect
) )

View File

@ -195,6 +195,8 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/vansante/go-ffprobe.v2 v2.0.2 h1:DdxSfFnlqeawPIVbIQEI6LR6OQHQNR7tNgWb2mWuC4w=
gopkg.in/vansante/go-ffprobe.v2 v2.0.2/go.mod h1:qF0AlAjk7Nqzqf3y333Ly+KxN3cKF2JqA3JT5ZheUGE=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=

View File

@ -10,6 +10,7 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/viktorstrate/photoview/api/graphql/models" "github.com/viktorstrate/photoview/api/graphql/models"
"github.com/viktorstrate/photoview/api/utils" "github.com/viktorstrate/photoview/api/utils"
"gopkg.in/vansante/go-ffprobe.v2"
) )
type PhotoDimensions struct { type PhotoDimensions struct {
@ -43,12 +44,13 @@ func (dimensions *PhotoDimensions) ThumbnailScale() PhotoDimensions {
} }
} }
// EncodeImageData is used to easily decode image data, with a cache so expensive operations are not repeated // EncodeMediaData is used to easily decode media data, with a cache so expensive operations are not repeated
type EncodeImageData struct { type EncodeMediaData struct {
media *models.Media media *models.Media
_photoImage image.Image _photoImage image.Image
_thumbnailImage image.Image _thumbnailImage image.Image
_contentType *MediaType _contentType *MediaType
_videoMetadata *ffprobe.Stream
} }
func EncodeImageJPEG(image image.Image, outputPath string, jpegQuality int) error { func EncodeImageJPEG(image image.Image, outputPath string, jpegQuality int) error {
@ -85,7 +87,7 @@ func GetPhotoDimensions(imagePath string) (*PhotoDimensions, error) {
} }
// ContentType reads the image to determine its content type // ContentType reads the image to determine its content type
func (img *EncodeImageData) ContentType() (*MediaType, error) { func (img *EncodeMediaData) ContentType() (*MediaType, error) {
if img._contentType != nil { if img._contentType != nil {
return img._contentType, nil return img._contentType, nil
} }
@ -99,7 +101,7 @@ func (img *EncodeImageData) ContentType() (*MediaType, error) {
return imgType, nil return imgType, nil
} }
func (img *EncodeImageData) EncodeHighRes(tx *sql.Tx, outputPath string) error { func (img *EncodeMediaData) EncodeHighRes(tx *sql.Tx, outputPath string) error {
contentType, err := img.ContentType() contentType, err := img.ContentType()
if err != nil { if err != nil {
return err return err
@ -154,7 +156,7 @@ func EncodeThumbnail(inputPath string, outputPath string) (*PhotoDimensions, err
} }
// PhotoImage reads and decodes the image file and saves it in a cache so the photo in only decoded once // PhotoImage reads and decodes the image file and saves it in a cache so the photo in only decoded once
func (img *EncodeImageData) photoImage(tx *sql.Tx) (image.Image, error) { func (img *EncodeMediaData) photoImage(tx *sql.Tx) (image.Image, error) {
if img._photoImage != nil { if img._photoImage != nil {
return img._photoImage, nil return img._photoImage, nil
} }

View File

@ -91,6 +91,7 @@ func (worker *FfmpegWorker) EncodeMp4(inputPath string, outputPath string) error
inputPath, inputPath,
"-vcodec", "h264", "-vcodec", "h264",
"-acodec", "aac", "-acodec", "aac",
"-vf", "scale='min(1080,iw)':'min(1080,ih)':force_original_aspect_ratio=decrease",
outputPath, outputPath,
} }

View File

@ -159,18 +159,18 @@ var fileExtensions = map[string]MediaType{
".fff": TypeFFF, ".fff": TypeFFF,
// Video formats // Video formats
"mp4": TypeMP4, ".mp4": TypeMP4,
"m4v": TypeMP4, ".m4v": TypeMP4,
"mpeg": TypeMPEG, ".mpeg": TypeMPEG,
"3gp": Type3GP, ".3gp": Type3GP,
"3g2": Type3G2, ".3g2": Type3G2,
"ogv": TypeOGV, ".ogv": TypeOGV,
"wmv": TypeWMV, ".wmv": TypeWMV,
"avi": TypeAVI, ".avi": TypeAVI,
"webm": TypeWEBM, ".webm": TypeWEBM,
"mov": TypeMOV, ".mov": TypeMOV,
"qt": TypeMOV, ".qt": TypeMOV,
"ts": TypeTS, ".ts": TypeTS,
} }
func (imgType *MediaType) isRaw() bool { func (imgType *MediaType) isRaw() bool {

View File

@ -44,7 +44,7 @@ func makePhotoURLChecker(tx *sql.Tx, mediaID int) (func(purpose models.MediaPurp
} }
func ProcessMedia(tx *sql.Tx, media *models.Media) (bool, error) { func ProcessMedia(tx *sql.Tx, media *models.Media) (bool, error) {
imageData := EncodeImageData{ imageData := EncodeMediaData{
media: media, media: media,
} }
@ -66,7 +66,7 @@ func ProcessMedia(tx *sql.Tx, media *models.Media) (bool, error) {
} }
} }
func processPhoto(tx *sql.Tx, imageData *EncodeImageData, photoCachePath *string) (bool, error) { func processPhoto(tx *sql.Tx, imageData *EncodeMediaData, photoCachePath *string) (bool, error) {
photo := imageData.media photo := imageData.media
@ -131,8 +131,7 @@ func processPhoto(tx *sql.Tx, imageData *EncodeImageData, photoCachePath *string
_, err = tx.Exec("INSERT INTO media_url (media_id, media_name, width, height, purpose, content_type) VALUES (?, ?, ?, ?, ?, ?)", _, err = tx.Exec("INSERT INTO media_url (media_id, media_name, width, height, purpose, content_type) VALUES (?, ?, ?, ?, ?, ?)",
photo.MediaID, highres_name, photoDimensions.Width, photoDimensions.Height, models.PhotoHighRes, "image/jpeg") photo.MediaID, highres_name, photoDimensions.Width, photoDimensions.Height, models.PhotoHighRes, "image/jpeg")
if err != nil { if err != nil {
log.Printf("Could not insert highres media url: %d, %s\n", photo.MediaID, path.Base(photo.Path)) return false, errors.Wrapf(err, "could not insert highres media url (%d, %s)", photo.MediaID, photo.Title)
return false, err
} }
} }
} else { } else {
@ -238,7 +237,7 @@ func makeMediaCacheDir(photo *models.Media) (*string, error) {
return &photoCachePath, nil return &photoCachePath, nil
} }
func saveOriginalPhotoToDB(tx *sql.Tx, photo *models.Media, imageData *EncodeImageData, photoDimensions *PhotoDimensions) error { func saveOriginalPhotoToDB(tx *sql.Tx, photo *models.Media, imageData *EncodeMediaData, photoDimensions *PhotoDimensions) error {
photoName := path.Base(photo.Path) photoName := path.Base(photo.Path)
photoBaseName := photoName[0 : len(photoName)-len(path.Ext(photoName))] photoBaseName := photoName[0 : len(photoName)-len(path.Ext(photoName))]
photoBaseExt := path.Ext(photoName) photoBaseExt := path.Ext(photoName)

View File

@ -1,15 +1,22 @@
package scanner package scanner
import ( import (
"context"
"database/sql" "database/sql"
"fmt"
"log" "log"
"path"
"strings"
"time"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/viktorstrate/photoview/api/graphql/models" "github.com/viktorstrate/photoview/api/graphql/models"
"github.com/viktorstrate/photoview/api/utils"
"gopkg.in/vansante/go-ffprobe.v2"
) )
func processVideo(tx *sql.Tx, imageData *EncodeImageData, videoCachePath *string) (bool, error) { func processVideo(tx *sql.Tx, mediaData *EncodeMediaData, videoCachePath *string) (bool, error) {
video := imageData.media video := mediaData.media
didProcess := false didProcess := false
log.Printf("Processing video: %s", video.Path) log.Printf("Processing video: %s", video.Path)
@ -25,10 +32,63 @@ func processVideo(tx *sql.Tx, imageData *EncodeImageData, videoCachePath *string
} }
if videoWebURL == nil { if videoWebURL == nil {
// TODO: Process web video web_video_name := fmt.Sprintf("web_video_%s_%s", path.Base(video.Path), utils.GenerateToken())
web_video_name = strings.ReplaceAll(web_video_name, ".", "_")
web_video_name = strings.ReplaceAll(web_video_name, " ", "_")
web_video_name = web_video_name + ".mp4"
webVideoPath := path.Join(*videoCachePath, web_video_name)
err = FfmpegCli.EncodeMp4(video.Path, webVideoPath)
if err != nil {
return false, errors.Wrapf(err, "could not encode mp4 video (%s)", video.Path)
}
webMetadata, err := readVideoMetadata(webVideoPath)
if err != nil {
return false, errors.Wrapf(err, "failed to read metadata for encoded web-video (%s)", video.Title)
}
_, err = tx.Exec("INSERT INTO media_url (media_id, media_name, width, height, purpose, content_type) VALUES (?, ?, ?, ?, ?, ?)",
video.MediaID, web_video_name, webMetadata.Width, webMetadata.Height, models.VideoWeb, "video/mp4")
if err != nil {
return false, errors.Wrapf(err, "failed to insert encoded web-video into database (%s)", video.Title)
}
} }
// TODO: Process video thumbnail // TODO: Process video thumbnail
return didProcess, nil return didProcess, nil
} }
func (enc *EncodeMediaData) VideoMetadata() (*ffprobe.Stream, error) {
if enc._videoMetadata != nil {
return enc._videoMetadata, nil
}
metadata, err := readVideoMetadata(enc.media.Path)
if err != nil {
return nil, err
}
enc._videoMetadata = metadata
return metadata, nil
}
func readVideoMetadata(videoPath string) (*ffprobe.Stream, error) {
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
defer cancelFn()
data, err := ffprobe.ProbeURL(ctx, videoPath)
if err != nil {
return nil, errors.Wrapf(err, "could not read video metadata (%s)", path.Base(videoPath))
}
stream := data.FirstVideoStream()
if stream == nil {
return nil, errors.Wrapf(err, "could not get stream from file metadata (%s)", path.Base(videoPath))
}
return stream, nil
}