server/repochecker/videos/video.go

151 lines
4.1 KiB
Go

package main
import (
"encoding/json"
"fmt"
"log"
"net/url"
"strconv"
ffmpeg "github.com/u2takey/ffmpeg-go"
"srs.epita.fr/fic-server/admin/sync"
"srs.epita.fr/fic-server/libfic"
)
type VideoInfo struct {
Streams []struct {
CodecType string `json:"codec_type"`
CodecName string `json:"codec_name"`
CodecLongName string `json:"codec_long_name"`
Duration string
NbFrames string `json:"nb_frames"`
Width int
Height int
} `json:"streams"`
}
func gcd(a, b int) int {
var bgcd func(a, b, res int) int
bgcd = func(a, b, res int) int {
switch {
case a == b:
return res * a
case a%2 == 0 && b%2 == 0:
return bgcd(a/2, b/2, 2*res)
case a%2 == 0:
return bgcd(a/2, b, res)
case b%2 == 0:
return bgcd(a, b/2, res)
case a > b:
return bgcd(a-b, b, res)
default:
return bgcd(a, b-a, res)
}
}
return bgcd(a, b, 1)
}
func checkResolutionVideo(e *fic.Exercice, exceptions *sync.CheckExceptions) (errs []error) {
i, ok := sync.GlobalImporter.(sync.LocalImporter)
if !ok {
log.Printf("Unable to load `videos-rules.so` as the current Importer is not a LocalImporter (%T).", sync.GlobalImporter)
return
}
if len(e.VideoURI) == 0 {
return
}
// Filter exceptions to only keep related to resolution.mp4
exceptions = exceptions.GetFileExceptions("resolution.mp4")
path, err := url.PathUnescape(e.VideoURI[9:])
if err != nil {
path = e.VideoURI
}
path = i.GetLocalPath(path)
data, err := ffmpeg.Probe(path)
if err != nil {
errs = append(errs, fmt.Errorf("unable to open %q: %w", path, err))
return
}
vInfo := &VideoInfo{}
err = json.Unmarshal([]byte(data), vInfo)
if err != nil {
panic(err)
}
video_seen := false
subtitles_seen := false
for _, s := range vInfo.Streams {
if s.CodecType == "video" {
video_seen = true
if (s.Width > 1920 || s.Height > 1080) && !exceptions.HasException(":size:above_maximum") {
errs = append(errs, fmt.Errorf("video track is too wide: %dx%d (maximum allowed: 1920x1080)", s.Width, s.Height))
}
ratio := s.Width * 10 / s.Height
if ratio < 13 || ratio > 19 && !exceptions.HasException(":size:strange_ratio") {
m := gcd(s.Width, s.Height)
errs = append(errs, fmt.Errorf("video track has a strange ratio: %d:%d. Is this really expected?", s.Width/m, s.Height/m))
}
if s.CodecName != "h264" {
errs = append(errs, fmt.Errorf("video codec has to be H264 (currently: %s)", s.CodecLongName))
}
duration, err := strconv.ParseFloat(s.Duration, 64)
if err == nil {
if duration < 45 && !exceptions.HasException(":duration:too_short") {
errs = append(errs, fmt.Errorf("video is too short"))
}
if duration > 450 && !exceptions.HasException(":duration:too_long") {
errs = append(errs, fmt.Errorf("video is too long"))
}
} else {
errs = append(errs, fmt.Errorf("invalid track duration: %q", s.Duration))
}
} else if s.CodecType == "subtitle" {
subtitles_seen = true
if s.CodecName != "mov_text" {
errs = append(errs, fmt.Errorf("subtitle format has to be MOV text/3GPP Timed Text (currently: %s)", s.CodecLongName))
}
nbframes, err := strconv.ParseInt(s.NbFrames, 10, 64)
if err == nil {
if nbframes < 5 && !exceptions.HasException(":subtitle:tiny") {
errs = append(errs, fmt.Errorf("too few subtitles"))
}
} else {
errs = append(errs, fmt.Errorf("invalid number of frame: %q", s.NbFrames))
}
} else if s.CodecType == "audio" {
if !exceptions.HasException(":audio:allowed") {
errs = append(errs, fmt.Errorf("an audio track is present, use subtitle for explainations"))
}
if s.CodecName != "aac" {
errs = append(errs, fmt.Errorf("audio codec has to be AAC (Advanced Audio Coding) (currently: %s)", s.CodecLongName))
}
} else {
errs = append(errs, fmt.Errorf("unknown track found of type %q", s.CodecType))
}
}
if !video_seen {
errs = append(errs, fmt.Errorf("no video track found"))
}
if !subtitles_seen && !exceptions.HasException(":subtitle:no_track") {
errs = append(errs, fmt.Errorf("no subtitles track found"))
} else if subtitles_seen {
errs = append(errs, CheckGrammarSubtitleTrack(path, e, exceptions)...)
}
return
}