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/mod v0.3.0 // 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
)

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-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
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/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=

View File

@ -10,6 +10,7 @@ import (
"github.com/pkg/errors"
"github.com/viktorstrate/photoview/api/graphql/models"
"github.com/viktorstrate/photoview/api/utils"
"gopkg.in/vansante/go-ffprobe.v2"
)
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
type EncodeImageData struct {
// EncodeMediaData is used to easily decode media data, with a cache so expensive operations are not repeated
type EncodeMediaData struct {
media *models.Media
_photoImage image.Image
_thumbnailImage image.Image
_contentType *MediaType
_videoMetadata *ffprobe.Stream
}
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
func (img *EncodeImageData) ContentType() (*MediaType, error) {
func (img *EncodeMediaData) ContentType() (*MediaType, error) {
if img._contentType != nil {
return img._contentType, nil
}
@ -99,7 +101,7 @@ func (img *EncodeImageData) ContentType() (*MediaType, error) {
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()
if err != nil {
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
func (img *EncodeImageData) photoImage(tx *sql.Tx) (image.Image, error) {
func (img *EncodeMediaData) photoImage(tx *sql.Tx) (image.Image, error) {
if img._photoImage != nil {
return img._photoImage, nil
}

View File

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

View File

@ -159,18 +159,18 @@ var fileExtensions = map[string]MediaType{
".fff": TypeFFF,
// Video formats
"mp4": TypeMP4,
"m4v": TypeMP4,
"mpeg": TypeMPEG,
"3gp": Type3GP,
"3g2": Type3G2,
"ogv": TypeOGV,
"wmv": TypeWMV,
"avi": TypeAVI,
"webm": TypeWEBM,
"mov": TypeMOV,
"qt": TypeMOV,
"ts": TypeTS,
".mp4": TypeMP4,
".m4v": TypeMP4,
".mpeg": TypeMPEG,
".3gp": Type3GP,
".3g2": Type3G2,
".ogv": TypeOGV,
".wmv": TypeWMV,
".avi": TypeAVI,
".webm": TypeWEBM,
".mov": TypeMOV,
".qt": TypeMOV,
".ts": TypeTS,
}
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) {
imageData := EncodeImageData{
imageData := EncodeMediaData{
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
@ -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 (?, ?, ?, ?, ?, ?)",
photo.MediaID, highres_name, photoDimensions.Width, photoDimensions.Height, models.PhotoHighRes, "image/jpeg")
if err != nil {
log.Printf("Could not insert highres media url: %d, %s\n", photo.MediaID, path.Base(photo.Path))
return false, err
return false, errors.Wrapf(err, "could not insert highres media url (%d, %s)", photo.MediaID, photo.Title)
}
}
} else {
@ -238,7 +237,7 @@ func makeMediaCacheDir(photo *models.Media) (*string, error) {
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)
photoBaseName := photoName[0 : len(photoName)-len(path.Ext(photoName))]
photoBaseExt := path.Ext(photoName)

View File

@ -1,15 +1,22 @@
package scanner
import (
"context"
"database/sql"
"fmt"
"log"
"path"
"strings"
"time"
"github.com/pkg/errors"
"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) {
video := imageData.media
func processVideo(tx *sql.Tx, mediaData *EncodeMediaData, videoCachePath *string) (bool, error) {
video := mediaData.media
didProcess := false
log.Printf("Processing video: %s", video.Path)
@ -25,10 +32,63 @@ func processVideo(tx *sql.Tx, imageData *EncodeImageData, videoCachePath *string
}
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
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
}