Skip to content

Commit

Permalink
feat: Better json to struct (#6)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
DblK committed Jul 28, 2022
1 parent 5c24711 commit 35622ab
Show file tree
Hide file tree
Showing 22 changed files with 420 additions and 230 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 2 additions & 25 deletions go/api_edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,38 +224,15 @@ 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
}
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)
}
}
}

Expand Down
8 changes: 4 additions & 4 deletions go/ffmpeg.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -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")
Expand Down
194 changes: 56 additions & 138 deletions go/model_asset.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,155 +26,73 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.

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: <ul> <li><a href=\"#tocs_videoasset\">VideoAsset</a></li> <li><a href=\"#tocs_imageasset\">ImageAsset</a></li> <li><a href=\"#tocs_titleasset\">TitleAsset</a></li> <li><a href=\"#tocs_HTMLAsset\">HTMLAsset</a></li> <li><a href=\"#tocs_audioasset\">AudioAsset</a></li> <li><a href=\"#tocs_lumaasset\">LumaAsset</a></li> </ul>
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. <ul> <li>`minimal`</li> <li>`blockbuster`</li> <li>`vogue`</li> <li>`sketchy`</li> <li>`skinny`</li> <li>`chunk`</li> <li>`chunkLight`</li> <li>`marker`</li> <li>`future`</li> <li>`subtitle`</li> </ul>
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. <ul> <li>`xx-small`</li> <li>`x-small`</li> <li>`small`</li> <li>`medium`</li> <li>`large`</li> <li>`x-large`</li> <li>`xx-large`</li> </ul>
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. <ul> <li>`top` - top (center)</li> <li>`topRight` - top right</li> <li>`right` - right (center)</li> <li>`bottomRight` - bottom right</li> <li>`bottom` - bottom (center)</li> <li>`bottomLeft` - bottom left</li> <li>`left` - left (center)</li> <li>`topLeft` - top left</li> <li>`center` - center</li> </ul>
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 <ul> <li>`fadeIn` - fade volume in only</li> <li>`fadeOut` - fade volume out only</li> <li>`fadeInFadeOut` - fade volume in and out</li> </ul>
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)
})
}
77 changes: 68 additions & 9 deletions go/model_clip.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,18 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
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.
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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))
Expand All @@ -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 {
Expand Down

0 comments on commit 35622ab

Please sign in to comment.