summaryrefslogtreecommitdiff
path: root/internal/media/video.go
blob: bffdfbbba7d6c9687d6e73400c0bf53003269899 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
/*
   GoToSocial
   Copyright (C) 2021-2023 GoToSocial Authors admin@gotosocial.org

   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/>.
*/

package media

import (
	"fmt"
	"io"
	"os"

	"github.com/abema/go-mp4"
)

type gtsVideo struct {
	frame     *gtsImage
	duration  float32 // in seconds
	bitrate   uint64
	framerate float32
}

// 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...
	tmp, err := os.CreateTemp(os.TempDir(), "gotosocial-")
	if err != nil {
		return nil, err
	}

	defer func() {
		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(tmp, r); err != nil {
		return nil, err
	}

	// 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(tmp)
	if err != nil {
		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
		}

		if w := int(tr.AVC.Width); w > width {
			width = w
		}

		if h := int(tr.AVC.Height); h > height {
			height = h
		}

		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 := float64(tr.Duration) / float64(tr.Timescale); d > float64(video.duration) {
			video.framerate = float32(len(tr.Samples)) / float32(d)
			video.duration = float32(d)
		}
	}

	// Check for empty video metadata.
	var empty []string
	if width == 0 {
		empty = append(empty, "width")
	}
	if height == 0 {
		empty = append(empty, "height")
	}
	if video.duration == 0 {
		empty = append(empty, "duration")
	}
	if video.framerate == 0 {
		empty = append(empty, "framerate")
	}
	if video.bitrate == 0 {
		empty = append(empty, "bitrate")
	}
	if len(empty) > 0 {
		return nil, fmt.Errorf("error determining video metadata: %v", empty)
	}

	// Create new empty "frame" image.
	// TODO: decode frame from video file.
	video.frame = blankImage(width, height)

	return &video, nil
}