gtsocial-umbx

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

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 }