diff options
Diffstat (limited to 'internal/media/video.go')
-rw-r--r-- | internal/media/video.go | 130 |
1 files changed, 46 insertions, 84 deletions
diff --git a/internal/media/video.go b/internal/media/video.go index bd624559b..bffdfbbba 100644 --- a/internal/media/video.go +++ b/internal/media/video.go @@ -19,63 +19,55 @@ package media import ( - "bytes" "fmt" - "image" - "image/color" - "image/draw" - "image/jpeg" "io" "os" "github.com/abema/go-mp4" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/log" ) -var thumbFill = color.RGBA{42, 43, 47, 0} // the color to fill video thumbnails with +type gtsVideo struct { + frame *gtsImage + duration float32 // in seconds + bitrate uint64 + framerate float32 +} -func decodeVideo(r io.Reader, contentType string) (*mediaMeta, error) { +// decodeVideoFrame decodes and returns an image from a single frame in the given video stream. +// (note: currently this only returns a blank image resized to fit video dimensions). +func decodeVideoFrame(r io.Reader) (*gtsVideo, error) { // We'll need a readseeker to decode the video. We can get a readseeker // without burning too much mem by first copying the reader into a temp file. // First create the file in the temporary directory... - tempFile, err := os.CreateTemp(os.TempDir(), "gotosocial-") + tmp, err := os.CreateTemp(os.TempDir(), "gotosocial-") if err != nil { - return nil, fmt.Errorf("could not create temporary file while decoding video: %w", err) + return nil, err } - tempFileName := tempFile.Name() - // Make sure to clean up the temporary file when we're done with it defer func() { - if err := tempFile.Close(); err != nil { - log.Errorf("could not close file %s: %s", tempFileName, err) - } - if err := os.Remove(tempFileName); err != nil { - log.Errorf("could not remove file %s: %s", tempFileName, err) - } + tmp.Close() + os.Remove(tmp.Name()) }() // Now copy the entire reader we've been provided into the // temporary file; we won't use the reader again after this. - if _, err := io.Copy(tempFile, r); err != nil { - return nil, fmt.Errorf("could not copy video reader into temporary file %s: %w", tempFileName, err) + if _, err := io.Copy(tmp, r); err != nil { + return nil, err } - var ( - width int - height int - duration float32 - framerate float32 - bitrate uint64 - ) - // probe the video file to extract useful metadata from it; for methodology, see: // https://github.com/abema/go-mp4/blob/7d8e5a7c5e644e0394261b0cf72fef79ce246d31/mp4tool/probe/probe.go#L85-L154 - info, err := mp4.Probe(tempFile) + info, err := mp4.Probe(tmp) if err != nil { - return nil, fmt.Errorf("could not probe temporary video file %s: %w", tempFileName, err) + return nil, fmt.Errorf("error probing tmp file %s: %w", tmp.Name(), err) } + var ( + width int + height int + video gtsVideo + ) + for _, tr := range info.Tracks { if tr.AVC == nil { continue @@ -89,72 +81,42 @@ func decodeVideo(r io.Reader, contentType string) (*mediaMeta, error) { height = h } - if br := tr.Samples.GetBitrate(tr.Timescale); br > bitrate { - bitrate = br - } else if br := info.Segments.GetBitrate(tr.TrackID, tr.Timescale); br > bitrate { - bitrate = br + if br := tr.Samples.GetBitrate(tr.Timescale); br > video.bitrate { + video.bitrate = br + } else if br := info.Segments.GetBitrate(tr.TrackID, tr.Timescale); br > video.bitrate { + video.bitrate = br } - if d := float32(tr.Duration) / float32(tr.Timescale); d > duration { - duration = d - framerate = float32(len(tr.Samples)) / duration + if d := float64(tr.Duration) / float64(tr.Timescale); d > float64(video.duration) { + video.framerate = float32(len(tr.Samples)) / float32(d) + video.duration = float32(d) } } - var errs gtserror.MultiError + // Check for empty video metadata. + var empty []string if width == 0 { - errs = append(errs, "video width could not be discovered") + empty = append(empty, "width") } - if height == 0 { - errs = append(errs, "video height could not be discovered") + empty = append(empty, "height") } - - if duration == 0 { - errs = append(errs, "video duration could not be discovered") + if video.duration == 0 { + empty = append(empty, "duration") } - - if framerate == 0 { - errs = append(errs, "video framerate could not be discovered") + if video.framerate == 0 { + empty = append(empty, "framerate") } - - if bitrate == 0 { - errs = append(errs, "video bitrate could not be discovered") + if video.bitrate == 0 { + empty = append(empty, "bitrate") } - - if errs != nil { - return nil, errs.Combine() + if len(empty) > 0 { + return nil, fmt.Errorf("error determining video metadata: %v", empty) } - return &mediaMeta{ - width: width, - height: height, - duration: duration, - framerate: framerate, - bitrate: bitrate, - size: height * width, - aspect: float32(width) / float32(height), - }, nil -} - -func deriveThumbnailFromVideo(height int, width int) (*mediaMeta, error) { - // create a rectangle with the same dimensions as the video - img := image.NewRGBA(image.Rect(0, 0, width, height)) - - // fill the rectangle with our desired fill color - draw.Draw(img, img.Bounds(), &image.Uniform{thumbFill}, image.Point{}, draw.Src) - - // we can get away with using extremely poor quality for this monocolor thumbnail - out := &bytes.Buffer{} - if err := jpeg.Encode(out, img, &jpeg.Options{Quality: 1}); err != nil { - return nil, fmt.Errorf("error encoding video thumbnail: %w", err) - } + // Create new empty "frame" image. + // TODO: decode frame from video file. + video.frame = blankImage(width, height) - return &mediaMeta{ - width: width, - height: height, - size: width * height, - aspect: float32(width) / float32(height), - small: out.Bytes(), - }, nil + return &video, nil } |