package main import ( "encoding/json" "fmt" "log" "net/url" "strconv" ffmpeg "github.com/u2takey/ffmpeg-go" "go.uber.org/multierr" "srs.epita.fr/fic-server/admin/sync" "srs.epita.fr/fic-server/libfic" ) type VideoInfo struct { Streams []struct { Index uint `json:"index"` 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 Tags map[string]string `json:"tags"` } `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 = multierr.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 := []int{} subtitles_seen := []int{} for idx, s := range vInfo.Streams { if s.CodecType == "video" { video_seen = append(video_seen, idx) if (s.Width > 1920 || s.Height > 1080) && !exceptions.HasException(":size:above_maximum") { errs = multierr.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 = multierr.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 = multierr.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 = multierr.Append(errs, fmt.Errorf("video is too short")) } if duration > 450 && !exceptions.HasException(":duration:too_long") { errs = multierr.Append(errs, fmt.Errorf("video is too long")) } } else { errs = multierr.Append(errs, fmt.Errorf("invalid track duration: %q", s.Duration)) } } else if s.CodecType == "subtitle" { subtitles_seen = append(subtitles_seen, idx) if s.CodecName != "mov_text" { errs = multierr.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 = multierr.Append(errs, fmt.Errorf("too few subtitles")) } } else { errs = multierr.Append(errs, fmt.Errorf("invalid number of frame: %q", s.NbFrames)) } } else if s.CodecType == "audio" { if !exceptions.HasException(":audio:allowed") { errs = multierr.Append(errs, fmt.Errorf("an audio track is present, use subtitle for explainations")) } if s.CodecName != "aac" { errs = multierr.Append(errs, fmt.Errorf("audio codec has to be AAC (Advanced Audio Coding) (currently: %s)", s.CodecLongName)) } } else { errs = multierr.Append(errs, fmt.Errorf("unknown track found of type %q", s.CodecType)) } } if len(video_seen) == 0 { errs = multierr.Append(errs, fmt.Errorf("no video track found")) } else if len(video_seen) > 1 { errs = multierr.Append(errs, fmt.Errorf("%d video tracks found, is it expected?", len(video_seen))) } if len(subtitles_seen) == 0 && !exceptions.HasException(":subtitle:no_track") { errs = multierr.Append(errs, fmt.Errorf("no subtitles track found")) } else if len(subtitles_seen) > 0 { for _, idx := range subtitles_seen { language := e.Language if lang, ok := vInfo.Streams[idx].Tags["language"]; e.Language != "" && (!ok || lang == "" || lang == "und") { errs = multierr.Append(errs, fmt.Errorf("subtitles track %d with no language defined", vInfo.Streams[idx].Index)) } else { language = lang } errs = multierr.Append(errs, CheckGrammarSubtitleTrack(path, vInfo.Streams[idx].Index, language, exceptions)) } if e.Language != "" && len(subtitles_seen) < 2 { errs = multierr.Append(errs, fmt.Errorf("subtitle tracks must exist in original language and translated, only one subtitle track found")) } } return }