diff --git a/internal/media/ffmpeg.go b/internal/media/ffmpeg.go index 58c2f9503..693b6f490 100644 --- a/internal/media/ffmpeg.go +++ b/internal/media/ffmpeg.go @@ -224,8 +224,11 @@ func ffprobe(ctx context.Context, filepath string) (*result, error) { // Show specifically stream codec names, types, frame rate, duration, dimens, and pixel format. "stream=codec_name,codec_type,r_frame_rate,duration_ts,width,height,pix_fmt" + ":" + - // Show orientation. - "tags=orientation", + // Show orientation tag. + "tags=orientation" + ":" + + + // Show rotation data. + "side_data=rotation", // Limit to reading the first // 1s of data looking for "rotation" @@ -490,7 +493,7 @@ func (res *ffprobeResult) Process() (*result, error) { } // Check extra packet / frame information - // for provided orientation (not always set). + // for provided orientation (if provided). for _, pf := range res.PacketsAndFrames { // Ensure frame contains tags. @@ -498,23 +501,24 @@ func (res *ffprobeResult) Process() (*result, error) { continue } - // Ensure orientation not - // already been specified. - if r.orientation != 0 { - return nil, errors.New("multiple sets of orientation data") - } - // Trim any space from orientation value. str := strings.TrimSpace(pf.Tags.Orientation) // Parse as integer value. - i, _ := strconv.Atoi(str) - if i < 0 || i >= 9 { + orient, _ := strconv.Atoi(str) + if orient < 0 || orient >= 9 { return nil, errors.New("invalid orientation data") } - // Set orientation. - r.orientation = i + // Ensure different value has + // not already been specified. + if r.orientation != 0 && + orient != r.orientation { + return nil, errors.New("multiple sets of orientation / rotation data") + } + + // Set new orientation. + r.orientation = orient } // Preallocate streams to max possible lengths. @@ -554,6 +558,57 @@ func (res *ffprobeResult) Process() (*result, error) { framerate = float32(num / den) } + // Check for embedded sidedata + // which may contain rotation data. + for _, d := range s.SideDataList { + + // Ensure frame side + // data IS rotation data. + if d.Rotation == 0 { + continue + } + + // Drop any decimal + // rotation value. + rot := int(d.Rotation) + + // Round rotation to multiple of 90. + // More granularity is not needed. + if q := rot % 90; q > 45 { + rot += (90 - q) + } else { + rot -= q + } + + // Drop any value above 360 + // or below -360, these are + // just repeat full turns. + // + // Then convert to + // orientation value. + var orient int + switch rot % 360 { + case 0: + orient = orientationNormal + case 90, -270: + orient = orientationRotate90 + case 180: + orient = orientationRotate180 + case 270, -90: + orient = orientationRotate270 + } + + // Ensure different value has + // not already been specified. + if r.orientation != 0 && + orient != r.orientation { + return nil, errors.New("multiple sets of orientation / rotation data") + } + + // Set new orientation. + r.orientation = orient + } + // Append video stream data to result. r.video = append(r.video, videoStream{ stream: stream{codec: s.CodecName}, @@ -580,6 +635,7 @@ type ffprobeResult struct { type ffprobePacketOrFrame struct { Type string `json:"type"` Tags ffprobeTags `json:"tags"` + // SideDataList []ffprobeSideData `json:"side_data_list"` } type ffprobeTags struct { @@ -587,13 +643,18 @@ type ffprobeTags struct { } type ffprobeStream struct { - CodecName string `json:"codec_name"` - CodecType string `json:"codec_type"` - PixFmt string `json:"pix_fmt"` - RFrameRate string `json:"r_frame_rate"` - DurationTS uint `json:"duration_ts"` - Width int `json:"width"` - Height int `json:"height"` + CodecName string `json:"codec_name"` + CodecType string `json:"codec_type"` + PixFmt string `json:"pix_fmt"` + RFrameRate string `json:"r_frame_rate"` + DurationTS uint `json:"duration_ts"` + Width int `json:"width"` + Height int `json:"height"` + SideDataList []ffprobeSideData `json:"side_data_list"` +} + +type ffprobeSideData struct { + Rotation float64 `json:"rotation"` } type ffprobeFormat struct {