2023-03-12 16:00:57 +01:00
|
|
|
// GoToSocial
|
|
|
|
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
|
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
//
|
|
|
|
// This program is free software: you can redistribute it and/or modify
|
|
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
|
|
// (at your option) any later version.
|
|
|
|
//
|
|
|
|
// This program is distributed in the hope that it will be useful,
|
|
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
// GNU Affero General Public License for more details.
|
|
|
|
//
|
|
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
2022-12-17 05:38:56 +01:00
|
|
|
|
|
|
|
package media
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
|
|
|
|
"github.com/abema/go-mp4"
|
2023-01-16 16:19:17 +01:00
|
|
|
"github.com/superseriousbusiness/gotosocial/internal/iotools"
|
|
|
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
2022-12-17 05:38:56 +01:00
|
|
|
)
|
|
|
|
|
2023-01-11 12:13:13 +01:00
|
|
|
type gtsVideo struct {
|
|
|
|
frame *gtsImage
|
|
|
|
duration float32 // in seconds
|
|
|
|
bitrate uint64
|
|
|
|
framerate float32
|
|
|
|
}
|
2022-12-17 05:38:56 +01:00
|
|
|
|
2023-01-11 12:13:13 +01:00
|
|
|
// 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) {
|
2023-01-16 16:19:17 +01:00
|
|
|
// we need a readseeker to decode the video...
|
|
|
|
tfs, err := iotools.TempFileSeeker(r)
|
2022-12-17 05:38:56 +01:00
|
|
|
if err != nil {
|
2023-01-16 16:19:17 +01:00
|
|
|
return nil, fmt.Errorf("error creating temp file seeker: %w", err)
|
2022-12-17 05:38:56 +01:00
|
|
|
}
|
|
|
|
defer func() {
|
2023-01-16 16:19:17 +01:00
|
|
|
if err := tfs.Close(); err != nil {
|
2023-02-17 12:02:29 +01:00
|
|
|
log.Errorf(nil, "error closing temp file seeker: %s", err)
|
2023-01-16 16:19:17 +01:00
|
|
|
}
|
2022-12-17 05:38:56 +01:00
|
|
|
}()
|
|
|
|
|
2022-12-22 11:48:28 +01:00
|
|
|
// 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
|
2023-01-16 16:19:17 +01:00
|
|
|
info, err := mp4.Probe(tfs)
|
2022-12-22 11:48:28 +01:00
|
|
|
if err != nil {
|
2023-01-16 16:19:17 +01:00
|
|
|
return nil, fmt.Errorf("error during mp4 probe: %w", err)
|
2022-12-17 05:38:56 +01:00
|
|
|
}
|
|
|
|
|
2023-01-11 12:13:13 +01:00
|
|
|
var (
|
2023-01-16 16:19:17 +01:00
|
|
|
width int
|
|
|
|
height int
|
|
|
|
videoBitrate uint64
|
|
|
|
audioBitrate uint64
|
|
|
|
video gtsVideo
|
2023-01-11 12:13:13 +01:00
|
|
|
)
|
|
|
|
|
2022-12-22 11:48:28 +01:00
|
|
|
for _, tr := range info.Tracks {
|
|
|
|
if tr.AVC == nil {
|
2023-01-16 16:19:17 +01:00
|
|
|
// audio track
|
|
|
|
if br := tr.Samples.GetBitrate(tr.Timescale); br > audioBitrate {
|
|
|
|
audioBitrate = br
|
|
|
|
} else if br := info.Segments.GetBitrate(tr.TrackID, tr.Timescale); br > audioBitrate {
|
|
|
|
audioBitrate = br
|
|
|
|
}
|
|
|
|
|
|
|
|
if d := float64(tr.Duration) / float64(tr.Timescale); d > float64(video.duration) {
|
|
|
|
video.duration = float32(d)
|
|
|
|
}
|
2022-12-22 11:48:28 +01:00
|
|
|
continue
|
|
|
|
}
|
2022-12-17 05:38:56 +01:00
|
|
|
|
2023-01-16 16:19:17 +01:00
|
|
|
// video track
|
2022-12-22 11:48:28 +01:00
|
|
|
if w := int(tr.AVC.Width); w > width {
|
|
|
|
width = w
|
2022-12-17 05:38:56 +01:00
|
|
|
}
|
|
|
|
|
2022-12-22 11:48:28 +01:00
|
|
|
if h := int(tr.AVC.Height); h > height {
|
|
|
|
height = h
|
2022-12-17 05:38:56 +01:00
|
|
|
}
|
|
|
|
|
2023-01-16 16:19:17 +01:00
|
|
|
if br := tr.Samples.GetBitrate(tr.Timescale); br > videoBitrate {
|
|
|
|
videoBitrate = br
|
|
|
|
} else if br := info.Segments.GetBitrate(tr.TrackID, tr.Timescale); br > videoBitrate {
|
|
|
|
videoBitrate = br
|
2022-12-22 11:48:28 +01:00
|
|
|
}
|
|
|
|
|
2023-01-11 12:13:13 +01:00
|
|
|
if d := float64(tr.Duration) / float64(tr.Timescale); d > float64(video.duration) {
|
|
|
|
video.framerate = float32(len(tr.Samples)) / float32(d)
|
|
|
|
video.duration = float32(d)
|
2022-12-22 11:48:28 +01:00
|
|
|
}
|
2022-12-17 05:38:56 +01:00
|
|
|
}
|
2022-12-22 11:48:28 +01:00
|
|
|
|
2023-01-16 16:19:17 +01:00
|
|
|
// overall bitrate should be audio + video combined
|
|
|
|
// (since they're both playing at the same time)
|
|
|
|
video.bitrate = audioBitrate + videoBitrate
|
|
|
|
|
2023-01-11 12:13:13 +01:00
|
|
|
// Check for empty video metadata.
|
|
|
|
var empty []string
|
2022-12-22 11:48:28 +01:00
|
|
|
if width == 0 {
|
2023-01-11 12:13:13 +01:00
|
|
|
empty = append(empty, "width")
|
2022-12-22 11:48:28 +01:00
|
|
|
}
|
|
|
|
if height == 0 {
|
2023-01-11 12:13:13 +01:00
|
|
|
empty = append(empty, "height")
|
2022-12-22 11:48:28 +01:00
|
|
|
}
|
2023-01-11 12:13:13 +01:00
|
|
|
if video.duration == 0 {
|
|
|
|
empty = append(empty, "duration")
|
2022-12-22 11:48:28 +01:00
|
|
|
}
|
2023-01-11 12:13:13 +01:00
|
|
|
if video.framerate == 0 {
|
|
|
|
empty = append(empty, "framerate")
|
2022-12-22 11:48:28 +01:00
|
|
|
}
|
2023-01-11 12:13:13 +01:00
|
|
|
if video.bitrate == 0 {
|
|
|
|
empty = append(empty, "bitrate")
|
2022-12-22 11:48:28 +01:00
|
|
|
}
|
2023-01-11 12:13:13 +01:00
|
|
|
if len(empty) > 0 {
|
|
|
|
return nil, fmt.Errorf("error determining video metadata: %v", empty)
|
2022-12-22 11:48:28 +01:00
|
|
|
}
|
|
|
|
|
2023-01-11 12:13:13 +01:00
|
|
|
// Create new empty "frame" image.
|
|
|
|
// TODO: decode frame from video file.
|
|
|
|
video.frame = blankImage(width, height)
|
2022-12-17 05:38:56 +01:00
|
|
|
|
2023-01-11 12:13:13 +01:00
|
|
|
return &video, nil
|
2022-12-17 05:38:56 +01:00
|
|
|
}
|