From 35622abc0e02a8644679a4d95de8d39bf3fd278e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Boulanouar?= Date: Thu, 28 Jul 2022 18:11:35 +0200 Subject: [PATCH] feat: Better json to struct (#6) * wip: new implementation of type asset * Add specific UnmarshalJSON function for output * fix index for subtitle * Move FPS output lower in the generation * better AssetType for clip --- README.md | 2 +- go/api_edit.go | 27 +---- go/ffmpeg.go | 8 +- go/model_asset.go | 194 +++++++++--------------------- go/model_clip.go | 77 ++++++++++-- go/model_crop.go | 20 +++ go/model_destinations.go | 40 +----- go/model_flip_transformation.go | 12 ++ go/model_mux_destination.go | 12 ++ go/model_offset.go | 14 +++ go/model_output.go | 74 +++++++++++- go/model_poster.go | 11 ++ go/model_range.go | 16 +++ go/model_rotate_transformation.go | 11 ++ go/model_size.go | 16 +++ go/model_skew_transformation.go | 14 +++ go/model_subtitle.go | 16 ++- go/model_thumbnail.go | 14 +++ go/model_transformation.go | 15 +++ go/model_transition.go | 12 ++ go/model_video_asset.go | 24 ++++ go/queue.go | 21 ++-- 22 files changed, 420 insertions(+), 230 deletions(-) diff --git a/README.md b/README.md index a7add8c..4ab791b 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ If you would like to sponsor features, bugs or prioritization, reach out to one * Use only the `stage` endpoint value until all features are implemented (See https://github.com/DblK/shottower/issues/1 for multiple endpoint handling) * 😎 Possible to burn subtitle into video clip * 😎 Allow to use local file from `url` filed (`file:///Users/dblk/clips/my_asset`) -* 😎 Add an endpoint `/dl/renders/:id` to download renders (instead of cdn/s3) +* 😎 Add an endpoint `/dl/{version}/renders/:id` to download renders (instead of cdn/s3) * 😎 Add other value in resolution (`360`, `480`, `540`, `720`) all with default `25 fps`. * [`Planned`] Allow to use ftp file from `url` filed (`ftp://user:password@dblk.org/mypath/my_asset`) * [`Planned`] Add destination to Youtube diff --git a/go/api_edit.go b/go/api_edit.go index fea9c81..8f6ff96 100644 --- a/go/api_edit.go +++ b/go/api_edit.go @@ -224,8 +224,8 @@ func (c *EditAPIController) PostRender(w http.ResponseWriter, r *http.Request) { var min float32 = 99999999999 var max float32 - for tIndex, track := range editParam.Timeline.Tracks { - for cIndex, clip := range track.Clips { + for _, track := range editParam.Timeline.Tracks { + for _, clip := range track.Clips { // Find min and max timing for the timeline if clip.Start < min { min = clip.Start @@ -233,29 +233,6 @@ func (c *EditAPIController) PostRender(w http.ResponseWriter, r *http.Request) { if clip.Start+clip.Length > max { max = clip.Start + clip.Length } - - // Convert clip Asset type - switch clip.Asset.Type { - case "video": - var videoAsset = &VideoAsset{ - Type: clip.Asset.Type, - Src: clip.Asset.Src, - Trim: clip.Asset.Trim, - Volume: clip.Asset.Volume, - Crop: clip.Asset.Crop, - Subtitle: clip.Asset.Subtitle, - } - editParam.Timeline.Tracks[tIndex].Clips[cIndex].TypedAsset = *videoAsset - case "image": - var imageAsset = &ImageAsset{ - Type: clip.Asset.Type, - Src: clip.Asset.Src, - Crop: clip.Asset.Crop, - } - editParam.Timeline.Tracks[tIndex].Clips[cIndex].TypedAsset = *imageAsset - default: - fmt.Println("Asset type not handled!!", clip.Asset.Type) - } } } diff --git a/go/ffmpeg.go b/go/ffmpeg.go index a9b8e39..df04415 100644 --- a/go/ffmpeg.go +++ b/go/ffmpeg.go @@ -557,10 +557,6 @@ func (s *FFMPEG) ToString() []string { parameters = append(parameters, "-i") parameters = append(parameters, s.GenerateBackground()) - // Add FPS output - parameters = append(parameters, "-r") - parameters = append(parameters, cast.ToString(s.fps)) - // Handle filter complex parameters = append(parameters, "-filter_complex") @@ -645,6 +641,10 @@ func (s *FFMPEG) ToString() []string { return make([]string, 0) } + // Add FPS output + parameters = append(parameters, "-r") + parameters = append(parameters, cast.ToString(s.fps)) + // FIXME: Deprecated field (fps_mode??) parameters = append(parameters, "-vsync") // https://stackoverflow.com/questions/18064604/frame-rate-very-high-for-a-muxer-not-efficiently-supporting-it parameters = append(parameters, "2") diff --git a/go/model_asset.go b/go/model_asset.go index 7a7f6b7..46bd34b 100644 --- a/go/model_asset.go +++ b/go/model_asset.go @@ -26,155 +26,73 @@ along with this program. If not, see . package openapi -import ( - "github.com/creasty/defaults" - "golang.org/x/exp/slices" +import "reflect" + +type AssetType int64 + +const ( + VideoAssetType AssetType = iota + ImageAssetType + TitleAssetType + HTMLAssetType + AudioAssetType + LumaAssetType + UnknownAssetType ) -// Asset - The type of asset to display for the duration of this Clip. Value must be one of: -type Asset struct { - - // The type of asset - set to `luma` for luma mattes. - Type string `json:"type"` - - // The luma matte source URL. The URL must be publicly accessible or include credentials. - Src string `json:"src,omitempty"` - - // The start trim point of the luma matte clip, in seconds (defaults to 0). Videos will start from the in trim point. A luma matte video will play until the file ends or the Clip length is reached. - Trim float32 `json:"trim,omitempty" default:"0"` - - // Set the volume for the audio clip between 0 and 1 where 0 is muted and 1 is full volume (defaults to 1). - Volume float32 `json:"volume,omitempty" default:"1"` - - Crop *Crop `json:"crop,omitempty"` - - // The title text string - i.e. \"My Title\". - Text string `json:"text,omitempty"` - - // Uses a preset to apply font properties and styling to the title. - Style string `json:"style,omitempty"` - - // Set the text color using hexadecimal color notation. Transparency is supported by setting the first two characters of the hex string (opposite to HTML), i.e. #80ffffff will be white with 50% transparency. - Color string `json:"color,omitempty"` - - // Set the relative size of the text using predefined sizes from xx-small to xx-large. - Size string `json:"size,omitempty"` - - // Apply a background color behind the HTML bounding box using. Set the text color using hexadecimal color notation. Transparency is supported by setting the first two characters of the hex string (opposite to HTML), i.e. #80ffffff will be white with 50% transparency. - Background string `json:"background,omitempty"` - - // Place the HTML in one of nine predefined positions within the HTML area. - Position string `json:"position,omitempty"` - - Offset *Offset `json:"offset,omitempty"` - - // The HTML text string. See list of [supported HTML tags](https://shotstack.io/docs/guide/architecting-an-application/html-support#supported-html-tags). - HTML string `json:"html,omitempty"` - - // The CSS text string to apply styling to the HTML. See list of [support CSS properties](https://shotstack.io/docs/guide/architecting-an-application/html-support#supported-css-properties). - CSS string `json:"css,omitempty"` - - // Set the width of the HTML asset bounding box in pixels. Text will wrap to fill the bounding box. - Width int32 `json:"width,omitempty"` - - // Set the width of the HTML asset bounding box in pixels. Text and elements will be masked if they exceed the height of the bounding box. - Height int32 `json:"height,omitempty"` - - // The effect to apply to the audio asset - Effect string `json:"effect,omitempty"` - - Subtitle *Subtitle `json:"subtitle,omitempty"` -} - -func (s *Asset) checkEnumValues() error { - styleValues := []string{"minimal", "blockbuster", "vogue", "sketchy", "skinny", "chunk", "chunkLight", "marker", "future", "subtitle"} - if s.Style != "" && !slices.Contains(styleValues, s.Style) { - return &EnumError{Schema: "Asset", Field: "Style", Value: s.Style} - } - - sizeValues := []string{"xx-small", "x-small", "small", "medium", "large", "x-large", "xx-large"} - if s.Size != "" && !slices.Contains(sizeValues, s.Size) { - return &EnumError{Schema: "Asset", Field: "Size", Value: s.Size} - } - - positionValues := []string{"top", "topRight", "right", "bottomRight", "bottom", "bottomLeft", "left", "topLeft", "center"} - if s.Position != "" && !slices.Contains(positionValues, s.Position) { - return &EnumError{Schema: "Asset", Field: "Position", Value: s.Position} +func (s AssetType) String() string { + switch s { // nolint:exhaustive + case VideoAssetType: + return "video" + case ImageAssetType: + return "image" + case TitleAssetType: + return "title" + case HTMLAssetType: + return "html" + case AudioAssetType: + return "audio" + case LumaAssetType: + return "luma" + + default: + return "unknown" } +} - effectValues := []string{"fadeIn", "fadeOut", "fadeInFadeOut"} - if s.Effect != "" && !slices.Contains(effectValues, s.Effect) { - return &EnumError{Schema: "Asset", Field: "Effect", Value: s.Effect} +func NewAsset(typeAsset string, obj map[string]interface{}) interface{} { + switch typeAsset { + case "video": + return NewVideoAsset(obj) } return nil } -// AssertAssetRequired checks if the required fields are not zero-ed -func AssertAssetRequired(obj *Asset) error { - elements := map[string]interface{}{ - "type": obj.Type, - } - for name, el := range elements { - if isZero := IsZeroValue(el); isZero { - return &RequiredError{Schema: "Asset", Field: name} - } - } - - if err := defaults.Set(obj); err != nil { - panic(err) - } - - if err := obj.checkEnumValues(); err != nil { - return err - } - - // Check mandatory field based on type - // https://shotstack.io/docs/api - switch obj.Type { - case "video": - if obj.Src == "" { - return &RequiredError{Schema: "Asset", Field: "src"} - } - case "image": - if obj.Src == "" { - return &RequiredError{Schema: "Asset", Field: "src"} - } - case "title": - if obj.Text == "" { - return &RequiredError{Schema: "Asset", Field: "text"} - } - case "html": - if obj.HTML == "" { - return &RequiredError{Schema: "Asset", Field: "html"} - } - case "audio": - if obj.Src == "" { - return &RequiredError{Schema: "Asset", Field: "src"} - } - case "luma": - if obj.Src == "" { - return &RequiredError{Schema: "Asset", Field: "src"} - } +func GetAssetType(asset interface{}) AssetType { + switch reflect.TypeOf(asset).String() { + case "*openapi.VideoAsset": + return VideoAssetType + case "*openapi.ImageAsset": + return ImageAssetType + case "*openapi.TitleAsset": + return TitleAssetType + case "*openapi.HTMLAsset": + return HTMLAssetType + case "*openapi.AudioAsset": + return AudioAssetType + case "*openapi.LumaAsset": + return LumaAssetType + default: + return UnknownAssetType } +} - if err := AssertCropRequired(obj.Crop); err != nil { - return err - } - if err := AssertOffsetRequired(obj.Offset); err != nil { - return err +// AssertAssetRequired checks if the required fields are not zero-ed +func AssertAssetRequired(obj interface{}) error { + switch GetAssetType(obj) { // nolint:exhaustive + case VideoAssetType: + return AssertVideoAssetRequired(obj.(VideoAsset)) } return nil } - -// AssertRecurseAssetRequired recursively checks if required fields are not zero-ed in a nested slice. -// Accepts only nested slice of Asset (e.g. [][]Asset), otherwise ErrTypeAssertionError is thrown. -func AssertRecurseAssetRequired(objSlice interface{}) error { - return AssertRecurseInterfaceRequired(objSlice, func(obj interface{}) error { - aAsset, ok := obj.(Asset) - if !ok { - return ErrTypeAssertionError - } - return AssertAssetRequired(&aAsset) - }) -} diff --git a/go/model_clip.go b/go/model_clip.go index 87e54a0..e781f2e 100644 --- a/go/model_clip.go +++ b/go/model_clip.go @@ -27,18 +27,18 @@ along with this program. If not, see . package openapi import ( + "encoding/json" "fmt" + "github.com/spf13/cast" "golang.org/x/exp/slices" ) // Clip - A clip is a container for a specific type of asset, i.e. a title, image, video, audio or html. You use a Clip to define when an asset will display on the timeline, how long it will play for and transitions, filters and effects to apply to it. type Clip struct { - Asset Asset `json:"asset"` + Asset interface{} `json:"asset"` - TypedAsset interface{} `json:"-"` - - // The start position of the Clip on the timeline, in seconds. + // The s{tart position of the Clip on the timeline, in seconds. Start float32 `json:"start"` // The length, in seconds, the Clip should play for. @@ -69,6 +69,64 @@ type Clip struct { Transform *Transformation `json:"transform,omitempty"` } +func NewClip(data map[string]interface{}, asset interface{}) *Clip { + clip := &Clip{ + Asset: asset, + } + + if data["start"] != nil { + clip.Start = cast.ToFloat32(data["start"].(float64)) + } + if data["length"] != nil { + clip.Length = cast.ToFloat32(data["length"].(float64)) + } + if data["fit"] != nil { + clip.Fit = data["fit"].(string) + } + if data["scale"] != nil { + scale := cast.ToFloat32(data["scale"].(float64)) + clip.Scale = &scale + } + if data["position"] != nil { + clip.Position = data["position"].(string) + } + if data["offset"] != nil { + clip.Offset = NewOffset(data["offset"].(map[string]interface{})) + } + if data["transition"] != nil { + clip.Transition = NewTransition(data["transition"].(map[string]interface{})) + } + if data["effect"] != nil { + clip.Effect = data["effect"].(string) + } + if data["filter"] != nil { + clip.Filter = data["filter"].(string) + } + if data["opacity"] != nil { + clip.Opacity = cast.ToFloat32(data["opacity"].(float64)) + } + if data["transform"] != nil { + clip.Transform = NewTransformation(data["transform"].(map[string]interface{})) + } + + return clip +} + +func (s *Clip) UnmarshalJSON(data []byte) error { + var obj map[string]interface{} + err := json.Unmarshal(data, &obj) + if err != nil { + return err + } + + var typeAsset = obj["asset"].(map[string]interface{})["type"].(string) + + asset := NewAsset(typeAsset, obj["asset"].(map[string]interface{})) + + *s = *NewClip(obj, asset) + return nil +} + func (s *Clip) checkEnumValues() error { fitValues := []string{"cover", "contain", "crop", "none"} if s.Fit != "" && !slices.Contains(fitValues, s.Fit) { @@ -98,9 +156,10 @@ func (s *Clip) ToFFMPEG(FFMPEGCommand FFMPEGCommand, sourceClip int, trackNumber var audioEffects []string var handled bool - switch s.Asset.Type { - case "video": - var currentAsset = s.TypedAsset.(VideoAsset) + var typeAsset = GetAssetType(s.Asset) + switch typeAsset { // nolint:exhaustive + case VideoAssetType: + var currentAsset = s.Asset.(*VideoAsset) if currentAsset.Subtitle != nil { effects = append(effects, FFMPEGCommand.ClipSubtitleBurn(sourceClip, trackNumber, currentClip, currentAsset.Subtitle.Index)) @@ -120,11 +179,11 @@ func (s *Clip) ToFFMPEG(FFMPEGCommand FFMPEGCommand, sourceClip int, trackNumber audioEffects = append(audioEffects, FFMPEGCommand.ClipAudioDelay(sourceClip, trackNumber, currentClip, s.Start*1000)) } - case "image": + case ImageAssetType: handled = true effects = append(effects, FFMPEGCommand.ClipImage(sourceClip, trackNumber, currentClip, 0, s.Length)) default: - fmt.Println("Type not handled for converting to FFMPEG", s.Asset.Type) + fmt.Println("Type not handled for converting to FFMPEG", typeAsset.String()) } if !handled { diff --git a/go/model_crop.go b/go/model_crop.go index 7ed8657..278f83a 100644 --- a/go/model_crop.go +++ b/go/model_crop.go @@ -26,6 +26,8 @@ along with this program. If not, see . package openapi +import "github.com/spf13/cast" + // Crop - Crop the sides of an asset by a relative amount. The size of the crop is specified using a scale between 0 and 1, relative to the screen width - i.e a left crop of 0.5 will crop half of the asset from the left, a top crop of 0.25 will crop the top by quarter of the asset. type Crop struct { @@ -42,6 +44,24 @@ type Crop struct { Right float32 `json:"right,omitempty"` } +func NewCrop(m map[string]interface{}) *Crop { + crop := &Crop{} + + if m["top"] != nil { + crop.Top = cast.ToFloat32(m["top"].(float64)) + } + if m["bottom"] != nil { + crop.Bottom = cast.ToFloat32(m["bottom"].(float64)) + } + if m["left"] != nil { + crop.Left = cast.ToFloat32(m["left"].(float64)) + } + if m["right"] != nil { + crop.Right = cast.ToFloat32(m["right"].(float64)) + } + return crop +} + // AssertCropRequired checks if the required fields are not zero-ed func AssertCropRequired(obj *Crop) error { return nil diff --git a/go/model_destinations.go b/go/model_destinations.go index f0f4977..761c388 100644 --- a/go/model_destinations.go +++ b/go/model_destinations.go @@ -26,43 +26,11 @@ along with this program. If not, see . package openapi -// Destinations - A destination is a location where output files can be sent to for serving or hosting. By default all rendered assets are automatically sent to the [Shotstack hosting destination](https://shotstack.io/docs/guide/serving-assets/hosting). You can add other destinations to send assets to. The following destinations are available: -type Destinations struct { - - // The destination to send rendered assets to - set to `mux` for Mux. - Provider string `json:"provider"` - - // Set to `true` to opt-out from the Shotstack hosting and CDN service. All files must be downloaded within 24 hours of rendering. - Exclude bool `json:"exclude,omitempty"` - - Options MuxDestinationOptions `json:"options,omitempty"` -} - -// AssertDestinationsRequired checks if the required fields are not zero-ed -func AssertDestinationsRequired(obj *Destinations) error { - elements := map[string]interface{}{ - "provider": obj.Provider, - } - for name, el := range elements { - if isZero := IsZeroValue(el); isZero { - return &RequiredError{Schema: "Destination", Field: name} - } +func NewDestination(provider string, obj map[string]interface{}) interface{} { + switch provider { + case "mux": + return NewMuxDestination(obj) } - if err := AssertMuxDestinationOptionsRequired(obj.Options); err != nil { - return err - } return nil } - -// AssertRecurseDestinationsRequired recursively checks if required fields are not zero-ed in a nested slice. -// Accepts only nested slice of Destinations (e.g. [][]Destinations), otherwise ErrTypeAssertionError is thrown. -func AssertRecurseDestinationsRequired(objSlice interface{}) error { - return AssertRecurseInterfaceRequired(objSlice, func(obj interface{}) error { - aDestinations, ok := obj.(Destinations) - if !ok { - return ErrTypeAssertionError - } - return AssertDestinationsRequired(&aDestinations) - }) -} diff --git a/go/model_flip_transformation.go b/go/model_flip_transformation.go index e658381..ec4d85c 100644 --- a/go/model_flip_transformation.go +++ b/go/model_flip_transformation.go @@ -36,6 +36,18 @@ type FlipTransformation struct { Vertical bool `json:"vertical,omitempty"` } +func NewFlipTransformation(m map[string]interface{}) *FlipTransformation { + transform := &FlipTransformation{} + + if m["horizontal"] != nil { + transform.Horizontal = m["horizontal"].(bool) + } + if m["vertical"] != nil { + transform.Vertical = m["vertical"].(bool) + } + return transform +} + // AssertFlipTransformationRequired checks if the required fields are not zero-ed func AssertFlipTransformationRequired(obj FlipTransformation) error { return nil diff --git a/go/model_mux_destination.go b/go/model_mux_destination.go index b07dbc8..e8c548b 100644 --- a/go/model_mux_destination.go +++ b/go/model_mux_destination.go @@ -35,6 +35,18 @@ type MuxDestination struct { Options MuxDestinationOptions `json:"options,omitempty"` } +func NewMuxDestination(obj map[string]interface{}) interface{} { + destination := &MuxDestination{ + Provider: obj["provider"].(string), + } + + if obj["options"] != nil { + destination.Options = obj["options"].(MuxDestinationOptions) + } + + return destination +} + // AssertMuxDestinationRequired checks if the required fields are not zero-ed func AssertMuxDestinationRequired(obj MuxDestination) error { elements := map[string]interface{}{ diff --git a/go/model_offset.go b/go/model_offset.go index e1344c9..d8f9219 100644 --- a/go/model_offset.go +++ b/go/model_offset.go @@ -26,6 +26,8 @@ along with this program. If not, see . package openapi +import "github.com/spf13/cast" + // Offset - Offsets the position of an asset horizontally or vertically by a relative distance. type Offset struct { @@ -36,6 +38,18 @@ type Offset struct { Y float32 `json:"y,omitempty"` } +func NewOffset(m map[string]interface{}) *Offset { + offset := &Offset{} + + if m["x"] != nil { + offset.X = cast.ToFloat32(m["x"].(float64)) + } + if m["y"] != nil { + offset.Y = cast.ToFloat32(m["y"].(float64)) + } + return offset +} + func (s *Offset) checkEnumValues() error { if s.X < -1 || s.X > 1 { return &EnumError{Schema: "Offset", Field: "X", Value: s.X} diff --git a/go/model_output.go b/go/model_output.go index 1e76423..e618375 100644 --- a/go/model_output.go +++ b/go/model_output.go @@ -27,7 +27,10 @@ along with this program. If not, see . package openapi import ( + "encoding/json" + "github.com/creasty/defaults" + "github.com/spf13/cast" "golang.org/x/exp/slices" ) @@ -63,7 +66,66 @@ type Output struct { Thumbnail *Thumbnail `json:"thumbnail,omitempty"` - Destinations []Destinations `json:"destinations,omitempty"` + Destinations []interface{} `json:"destinations,omitempty"` +} + +func NewOutput(data map[string]interface{}) *Output { + output := &Output{ + Format: data["format"].(string), + } + + if data["resolution"] != nil { + output.Resolution = data["resolution"].(string) + } + if data["aspectRatio"] != nil { + output.AspectRatio = data["aspectRatio"].(string) + } + if data["size"] != nil { + *output.Size = *NewSize(data["size"].(map[string]interface{})) + } + if data["fps"] != nil { + fps := cast.ToFloat32(data["fps"].(float64)) + output.Fps = &fps + } + if data["scaleTo"] != nil { + output.ScaleTo = data["scaleTo"].(string) + } + if data["quality"] != nil { + output.Quality = data["quality"].(string) + } + if data["repeat"] != nil { + output.Repeat = data["repeat"].(bool) + } + if data["range"] != nil { + *output.Range = *NewRange(data["range"].(map[string]interface{})) + } + if data["poster"] != nil { + *output.Poster = *NewPoster(data["poster"].(map[string]interface{})) + } + if data["thumbnail"] != nil { + *output.Thumbnail = *NewThumbnail(data["thumbnail"].(map[string]interface{})) + } + + if data["destinations"] != nil { + for _, dest := range data["destinations"].([]map[string]interface{}) { + var provider = dest["provider"].(string) + destination := NewDestination(provider, dest) + output.Destinations = append(output.Destinations, destination) + } + } + return output +} + +func (s *Output) UnmarshalJSON(data []byte) error { + var obj map[string]interface{} + err := json.Unmarshal(data, &obj) + if err != nil { + return err + } + + *s = *NewOutput(obj) + + return nil } func (s *Output) checkEnumValues() error { @@ -131,11 +193,11 @@ func AssertOutputRequired(obj *Output) error { if err := AssertThumbnailRequired(obj.Thumbnail); err != nil { return err } - for i := range obj.Destinations { - if err := AssertDestinationsRequired(&obj.Destinations[i]); err != nil { - return err - } - } + // for i := range obj.Destinations { + // if err := AssertDestinationsRequired(&obj.Destinations[i]); err != nil { + // return err + // } + // } return nil } diff --git a/go/model_poster.go b/go/model_poster.go index b5513cc..b37fe55 100644 --- a/go/model_poster.go +++ b/go/model_poster.go @@ -26,6 +26,8 @@ along with this program. If not, see . package openapi +import "github.com/spf13/cast" + // Poster - Generate a poster image for the video at a specific point from the timeline. The poster image size will match the size of the output video. type Poster struct { @@ -33,6 +35,15 @@ type Poster struct { Capture float32 `json:"capture"` } +func NewPoster(m map[string]interface{}) *Poster { + poster := &Poster{} + + if m["capture"] != nil { + poster.Capture = cast.ToFloat32(m["capture"].(float64)) + } + return poster +} + func (s *Poster) checkEnumValues() error { if s.Capture < 0 { return &EnumError{Schema: "Poster", Field: "Capture", Value: s.Capture} diff --git a/go/model_range.go b/go/model_range.go index f386234..a10bf0d 100644 --- a/go/model_range.go +++ b/go/model_range.go @@ -26,6 +26,8 @@ along with this program. If not, see . package openapi +import "github.com/spf13/cast" + // Range - Specify a time range to render, i.e. to render only a portion of a video or audio file. Omit this setting to export the entire video. Range can also be used to render a frame at a specific time point - setting a range and output format as `jpg` will output a single frame image at the range `start` point. type Range struct { @@ -36,6 +38,20 @@ type Range struct { Length *float32 `json:"length,omitempty"` } +func NewRange(m map[string]interface{}) *Range { + size := &Range{} + + if m["start"] != nil { + start := cast.ToFloat32(m["start"].(float64)) + size.Start = &start + } + if m["length"] != nil { + length := cast.ToFloat32(m["length"].(float64)) + size.Length = &length + } + return size +} + func (s *Range) checkEnumValues() error { if s.Start != nil { if *s.Start < 0 { diff --git a/go/model_rotate_transformation.go b/go/model_rotate_transformation.go index cec5d94..bd3da6c 100644 --- a/go/model_rotate_transformation.go +++ b/go/model_rotate_transformation.go @@ -26,6 +26,8 @@ along with this program. If not, see . package openapi +import "github.com/spf13/cast" + // RotateTransformation - Rotate a clip by the specified angle in degrees. Rotation origin is set based on the clips `position`. type RotateTransformation struct { @@ -33,6 +35,15 @@ type RotateTransformation struct { Angle int32 `json:"angle,omitempty"` } +func NewRotateTransformation(m map[string]interface{}) *RotateTransformation { + transform := &RotateTransformation{} + + if m["angle"] != nil { + transform.Angle = cast.ToInt32(m["angle"].(float64)) + } + return transform +} + func (s *RotateTransformation) checkEnumValues() error { if s.Angle < -360 || s.Angle > 360 { return &EnumError{Schema: "Soundtrack", Field: "Angle", Value: s.Angle} diff --git a/go/model_size.go b/go/model_size.go index ca48f6d..4af01ae 100644 --- a/go/model_size.go +++ b/go/model_size.go @@ -28,6 +28,8 @@ package openapi import ( "math" + + "github.com/spf13/cast" ) // Size - Set a custom size for a video or image. When using a custom size omit the `resolution` and `aspectRatio`. Custom sizes must be divisible by 2 based on the encoder specifications. @@ -40,6 +42,20 @@ type Size struct { Height *int32 `json:"height,omitempty"` } +func NewSize(m map[string]interface{}) *Size { + size := &Size{} + + if m["width"] != nil { + width := cast.ToInt32(m["width"].(float64)) + size.Width = &width + } + if m["height"] != nil { + height := cast.ToInt32(m["height"].(float64)) + size.Height = &height + } + return size +} + func (s *Size) checkEnumValues() error { if s.Width != nil { if *s.Width < 2 || *s.Width > 4096 || math.Mod(float64(*s.Width), 2) != 0 { diff --git a/go/model_skew_transformation.go b/go/model_skew_transformation.go index 1818851..40f3c67 100644 --- a/go/model_skew_transformation.go +++ b/go/model_skew_transformation.go @@ -26,6 +26,8 @@ along with this program. If not, see . package openapi +import "github.com/spf13/cast" + // SkewTransformation - Skew a clip so its edges are sheared at an angle. Use values between 0 and 3. Over 3 the clip will be skewed almost flat. type SkewTransformation struct { @@ -36,6 +38,18 @@ type SkewTransformation struct { Y float32 `json:"y,omitempty"` } +func NewSkewTransformation(m map[string]interface{}) *SkewTransformation { + transform := &SkewTransformation{} + + if m["x"] != nil { + transform.X = cast.ToFloat32(m["x"].(float64)) + } + if m["y"] != nil { + transform.X = cast.ToFloat32(m["x"].(float64)) + } + return transform +} + func (s *SkewTransformation) checkEnumValues() error { if s.X < 0 || s.X > 3 { return &EnumError{Schema: "Soundtrack", Field: "X", Value: s.X} diff --git a/go/model_subtitle.go b/go/model_subtitle.go index f27fd10..88369a5 100644 --- a/go/model_subtitle.go +++ b/go/model_subtitle.go @@ -26,13 +26,25 @@ along with this program. If not, see . package openapi -import "github.com/creasty/defaults" +import ( + "github.com/creasty/defaults" + "github.com/spf13/cast" +) // Subtitle - Subtitle allow to burn a specific subtitle into the video type Subtitle struct { // Index of the subtitle stream (Default to 0). - Index int `json:"index,omitempty" default:"0"` + Index int `json:"index" default:"0"` +} + +func NewSubtitle(m map[string]interface{}) *Subtitle { + subtitle := &Subtitle{} + + if m["index"] != nil { + subtitle.Index = cast.ToInt(m["index"].(float64)) + } + return subtitle } func (s *Subtitle) checkEnumValues() error { diff --git a/go/model_thumbnail.go b/go/model_thumbnail.go index 8f816f3..93bc8df 100644 --- a/go/model_thumbnail.go +++ b/go/model_thumbnail.go @@ -26,6 +26,8 @@ along with this program. If not, see . package openapi +import "github.com/spf13/cast" + // Thumbnail - Generate a thumbnail image for the video or image at a specific point from the timeline. type Thumbnail struct { @@ -36,6 +38,18 @@ type Thumbnail struct { Scale float32 `json:"scale"` } +func NewThumbnail(m map[string]interface{}) *Thumbnail { + thumbnail := &Thumbnail{} + + if m["capture"] != nil { + thumbnail.Capture = cast.ToFloat32(m["capture"].(float64)) + } + if m["scale"] != nil { + thumbnail.Scale = cast.ToFloat32(m["scale"].(float64)) + } + return thumbnail +} + func (s *Thumbnail) checkEnumValues() error { if s.Capture < 0 { return &EnumError{Schema: "Thumbnail", Field: "Capture", Value: s.Capture} diff --git a/go/model_transformation.go b/go/model_transformation.go index 3be43ee..2b9d347 100644 --- a/go/model_transformation.go +++ b/go/model_transformation.go @@ -35,6 +35,21 @@ type Transformation struct { Flip FlipTransformation `json:"flip,omitempty"` } +func NewTransformation(m map[string]interface{}) *Transformation { + transform := &Transformation{} + + if m["rotate"] != nil { + transform.Rotate = *NewRotateTransformation(m["rotate"].(map[string]interface{})) + } + if m["skew"] != nil { + transform.Skew = *NewSkewTransformation(m["skew"].(map[string]interface{})) + } + if m["flip"] != nil { + transform.Flip = *NewFlipTransformation(m["flip"].(map[string]interface{})) + } + return transform +} + // AssertTransformationRequired checks if the required fields are not zero-ed func AssertTransformationRequired(obj *Transformation) error { if obj == nil { diff --git a/go/model_transition.go b/go/model_transition.go index 1d55490..1ea3e53 100644 --- a/go/model_transition.go +++ b/go/model_transition.go @@ -38,6 +38,18 @@ type Transition struct { Out string `json:"out,omitempty"` } +func NewTransition(m map[string]interface{}) *Transition { + transition := &Transition{} + + if m["in"] != nil { + transition.In = m["in"].(string) + } + if m["out"] != nil { + transition.Out = m["out"].(string) + } + return transition +} + func (s *Transition) checkEnumValues() error { inValues := []string{"fade", "reveal", "wipeLeft", "wipeRight", "slideLeft", "slideRight", "slideUp", "slideDown", "carouselLeft", "carouselRight", "carouselUp", "carouselDown", "shuffleTopRight", "shuffleRightTop", "shuffleRightBottom", "shuffleBottomRight", "shuffleBottomLeft", "shuffleLeftBottom", "shuffleLeftTop", "zoom"} if s.In != "" && !slices.Contains(inValues, s.In) { diff --git a/go/model_video_asset.go b/go/model_video_asset.go index 8957f66..43644e6 100644 --- a/go/model_video_asset.go +++ b/go/model_video_asset.go @@ -28,6 +28,7 @@ package openapi import ( "github.com/creasty/defaults" + "github.com/spf13/cast" ) // VideoAsset - The VideoAsset is used to create video sequences from video files. The src must be a publicly accessible URL to a video resource such as an mp4 file. @@ -50,6 +51,29 @@ type VideoAsset struct { Subtitle *Subtitle `json:"subtitle,omitempty"` } +func NewVideoAsset(m map[string]interface{}) *VideoAsset { + videoAsset := &VideoAsset{ + Type: m["type"].(string), + } + + if m["src"] != nil { + videoAsset.Src = m["src"].(string) + } + if m["trim"] != nil { + videoAsset.Trim = cast.ToFloat32(m["trim"].(float64)) + } + if m["volume"] != nil { + videoAsset.Volume = cast.ToFloat32(m["volume"].(float64)) + } + if m["crop"] != nil { + videoAsset.Crop = NewCrop(m["crop"].(map[string]interface{})) + } + if m["subtitle"] != nil { + videoAsset.Subtitle = NewSubtitle(m["subtitle"].(map[string]interface{})) + } + return videoAsset +} + // AssertVideoAssetRequired checks if the required fields are not zero-ed func AssertVideoAssetRequired(obj VideoAsset) error { elements := map[string]interface{}{ diff --git a/go/queue.go b/go/queue.go index 8d6f067..7c827dd 100644 --- a/go/queue.go +++ b/go/queue.go @@ -155,18 +155,21 @@ func (s *ProcessingQueue) FetchAssets(queue *RenderQueue) { for _, track := range queue.Data.Timeline.Tracks { for _, clip := range track.Clips { // fmt.Println(tIndex, cIndex, clip.Asset.Type) - switch clip.Asset.Type { - case "video": - var fileName = assetFiles[clip.Asset.Src] + + var typeAsset = GetAssetType(clip.Asset) + switch typeAsset { // nolint:exhaustive + case VideoAssetType: + var asset = clip.Asset.(*VideoAsset) + var fileName = assetFiles[asset.Src] if fileName == "" { - url, _ := url.Parse(clip.Asset.Src) + url, _ := url.Parse(asset.Src) if url.Scheme == "file" { - fileName = clip.Asset.Src[7:] + fileName = asset.Src[7:] } else { var err error - fileName, err = s.DownloadFile(clip.Asset.Src) + fileName, err = s.DownloadFile(asset.Src) if err != nil { fmt.Println("Error while downloading asset", err) hasError = true @@ -175,9 +178,9 @@ func (s *ProcessingQueue) FetchAssets(queue *RenderQueue) { } if !hasError { - fmt.Println("Asset downloaded: "+clip.Asset.Src, fileName) + fmt.Println("Asset downloaded: "+asset.Src, fileName) queue.LocalResources = append(queue.LocalResources, fileName) - assetFiles[clip.Asset.Src] = fileName + assetFiles[asset.Src] = fileName } // case "image": @@ -188,7 +191,7 @@ func (s *ProcessingQueue) FetchAssets(queue *RenderQueue) { // fmt.Println("TODO: Download asset") // } default: - fmt.Println("Unhandled asset type", clip.Asset.Type) + fmt.Println("Unhandled asset type", typeAsset.String()) } } }