Encode videos for web using ffmpeg
This commit is contained in:
parent
0ab6048151
commit
26a5c5ac29
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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=
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue