video.go (3755B)
1 // GoToSocial 2 // Copyright (C) GoToSocial Authors admin@gotosocial.org 3 // SPDX-License-Identifier: AGPL-3.0-or-later 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package media 19 20 import ( 21 "fmt" 22 "io" 23 24 "github.com/abema/go-mp4" 25 "github.com/superseriousbusiness/gotosocial/internal/iotools" 26 "github.com/superseriousbusiness/gotosocial/internal/log" 27 ) 28 29 type gtsVideo struct { 30 frame *gtsImage 31 duration float32 // in seconds 32 bitrate uint64 33 framerate float32 34 } 35 36 // decodeVideoFrame decodes and returns an image from a single frame in the given video stream. 37 // (note: currently this only returns a blank image resized to fit video dimensions). 38 func decodeVideoFrame(r io.Reader) (*gtsVideo, error) { 39 // we need a readseeker to decode the video... 40 tfs, err := iotools.TempFileSeeker(r) 41 if err != nil { 42 return nil, fmt.Errorf("error creating temp file seeker: %w", err) 43 } 44 defer func() { 45 if err := tfs.Close(); err != nil { 46 log.Errorf(nil, "error closing temp file seeker: %s", err) 47 } 48 }() 49 50 // probe the video file to extract useful metadata from it; for methodology, see: 51 // https://github.com/abema/go-mp4/blob/7d8e5a7c5e644e0394261b0cf72fef79ce246d31/mp4tool/probe/probe.go#L85-L154 52 info, err := mp4.Probe(tfs) 53 if err != nil { 54 return nil, fmt.Errorf("error during mp4 probe: %w", err) 55 } 56 57 var ( 58 width int 59 height int 60 videoBitrate uint64 61 audioBitrate uint64 62 video gtsVideo 63 ) 64 65 for _, tr := range info.Tracks { 66 if tr.AVC == nil { 67 // audio track 68 if br := tr.Samples.GetBitrate(tr.Timescale); br > audioBitrate { 69 audioBitrate = br 70 } else if br := info.Segments.GetBitrate(tr.TrackID, tr.Timescale); br > audioBitrate { 71 audioBitrate = br 72 } 73 74 if d := float64(tr.Duration) / float64(tr.Timescale); d > float64(video.duration) { 75 video.duration = float32(d) 76 } 77 continue 78 } 79 80 // video track 81 if w := int(tr.AVC.Width); w > width { 82 width = w 83 } 84 85 if h := int(tr.AVC.Height); h > height { 86 height = h 87 } 88 89 if br := tr.Samples.GetBitrate(tr.Timescale); br > videoBitrate { 90 videoBitrate = br 91 } else if br := info.Segments.GetBitrate(tr.TrackID, tr.Timescale); br > videoBitrate { 92 videoBitrate = br 93 } 94 95 if d := float64(tr.Duration) / float64(tr.Timescale); d > float64(video.duration) { 96 video.framerate = float32(len(tr.Samples)) / float32(d) 97 video.duration = float32(d) 98 } 99 } 100 101 // overall bitrate should be audio + video combined 102 // (since they're both playing at the same time) 103 video.bitrate = audioBitrate + videoBitrate 104 105 // Check for empty video metadata. 106 var empty []string 107 if width == 0 { 108 empty = append(empty, "width") 109 } 110 if height == 0 { 111 empty = append(empty, "height") 112 } 113 if video.duration == 0 { 114 empty = append(empty, "duration") 115 } 116 if video.framerate == 0 { 117 empty = append(empty, "framerate") 118 } 119 if video.bitrate == 0 { 120 empty = append(empty, "bitrate") 121 } 122 if len(empty) > 0 { 123 return nil, fmt.Errorf("error determining video metadata: %v", empty) 124 } 125 126 // Create new empty "frame" image. 127 // TODO: decode frame from video file. 128 video.frame = blankImage(width, height) 129 130 return &video, nil 131 }