Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add youtube destination for output #4

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,5 +70,8 @@ issues:
- linters:
- goconst
text: "string `audio` has"
- linters:
- goconst
text: "string `unknown` has"
include:
# - EXC0002
2 changes: 2 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"errcheck",
"errorlint",
"exportloopref",
"faststart",
"funlen",
"gifs",
"gochecknoglobals",
Expand All @@ -44,6 +45,7 @@
"luma",
"lumaasset",
"lumamattes",
"movflags",
"nakedret",
"noctx",
"openapi",
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ If you would like to sponsor features, bugs or prioritization, reach out to one
* 😎 Allow to use local file from `url` filed (`file:///Users/dblk/clips/my_asset`)
* 😎 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`.
* 😎 Add destination to Youtube
* [`Planned`] Allow to use ftp file from `url` filed (`ftp://user:[email protected]/mypath/my_asset`)
* [`Planned`] Add destination to Youtube

### Shotstack implementation progress

Expand Down Expand Up @@ -82,7 +82,7 @@ At the end of the road this section should either disappear or be full of `Yes`
| Output | range | Not yet | |
| Output | poster | Not yet | |
| Output | thumbnail | Not yet | |
| Output | destinations | Not yet | |
| Output | destinations | Partial 🛠 | `shotstack` won't be implemented. |

#### Endpoint implementation

Expand Down
39 changes: 37 additions & 2 deletions api/openapi.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
openapi: 3.0.0
info:
description: "Shottowwer is the open source version of Shotstack which is a video, image and audio editing service that allows\
description: "Shottower is the open source version of Shotstack which is a video, image and audio editing service that allows\
\ for the automated\ngeneration of videos, images and audio using JSON and a RESTful\
\ API.\n\nYou arrange and configure an edit and POST it to the API which will\
\ render your media and provide a file \nlocation when complete.\n\nFor more details\
Expand Down Expand Up @@ -1906,12 +1906,14 @@ components:
anyOf:
- $ref: '#/components/schemas/ShotstackDestination'
- $ref: '#/components/schemas/MuxDestination'
- $ref: '#/components/schemas/YoutubeDestination'
description: "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:\n <ul>\n <li><a href=\"#tocs_shotstackdestination\">DestinationShotstack</a></li>\n\
\ <li><a href=\"#tocs_muxdestination\">DestinationMux</a></li>\n </ul>"
\ <li><a href=\"#tocs_muxdestination\">DestinationMux</a></li>\n
\ <li><a href=\"#tocs_youtubedestination\">DestinationYoutube</a></li>\n</ul>"
discriminator:
propertyName: destinations
type: object
Expand Down Expand Up @@ -1965,6 +1967,39 @@ components:
- signed
type: string
type: array
YoutubeDestination:
description: "Send rendered videos to [Youtube](https://www.youtube.com/) video\
\ hosting and streaming service. Add the `youtube` destination provider to send\
\ the output video to Youtube. Youtube credentials are required and added inside Options for now in the\
\ request."
properties:
provider:
default: youtube
description: The destination to send rendered assets to - set to `youtube` for
Youtube.
example: youtube
type: string
options:
$ref: '#/components/schemas/YoutubeDestinationOptions'
required:
- provider
type: object
YoutubeDestinationOptions:
description: Pass additional options to control how Youtube processes video.
properties:
title:
description: The title of the video
type: string
maxLength: 100
description:
description: The description of the video
type: string
maxLength: 5000
privacy:
description: The privacy of the video
enum:
- unlisted
type: string
Template:
description: "A template is a saved [Edit](#tocs_edit) than can be loaded and\
\ re-used."
Expand Down
36 changes: 24 additions & 12 deletions go/ffmpeg.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,19 @@ type FFMPEGTrack struct {
}

type FFMPEG struct {
src []FFMPEGSource
defaultParams bool
tracks []FFMPEGTrack
size Size
format string
fps float32
hasOverlay bool
backgroundColor string
overlayFillerCounter int
fillerCounter int
outputName string
duration float32
src []FFMPEGSource
defaultParams bool
tracks []FFMPEGTrack
size Size
format string
fps float32
hasOverlay bool
backgroundColor string
overlayFillerCounter int
fillerCounter int
outputName string
duration float32
hasYoutubeDestination bool
}

func NewFFMPEGCommand() FFMPEGCommand {
Expand Down Expand Up @@ -156,6 +157,11 @@ func (s *FFMPEG) ClipAudioMerge(sourceClip int, trackNumber int, clipNumber int,
return clipEffect
}

func (s *FFMPEG) HasYoutubeDestination() error {
s.hasYoutubeDestination = true
return nil
}

func (s *FFMPEG) SetOutputFormat(format string) error {
s.format = format
return nil
Expand Down Expand Up @@ -696,6 +702,12 @@ func (s *FFMPEG) ToString() []string {
parameters = append(parameters, "-vsync") // https://stackoverflow.com/questions/18064604/frame-rate-very-high-for-a-muxer-not-efficiently-supporting-it
parameters = append(parameters, "2")

// Help youtube to re-encode before upload complete (https://trac.ffmpeg.org/wiki/Encode/H.264)
if s.hasYoutubeDestination {
parameters = append(parameters, "-movflags")
parameters = append(parameters, "+faststart")
}

var outputName = s.generateOutputName()

parameters = append(parameters, outputName)
Expand Down
50 changes: 49 additions & 1 deletion go/model_destinations.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,59 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.

package openapi

import (
"reflect"
)

type DestinationProviderType int64

const (
MuxDestinationType DestinationProviderType = iota
YoutubeDestinationType
UnknownDestinationType
)

func (s DestinationProviderType) String() string {
switch s { // nolint:exhaustive
case MuxDestinationType:
return "mux"
case YoutubeDestinationType:
return "youtube"

default:
return "unknown"
}
}

func NewDestination(provider string, obj map[string]interface{}) interface{} {
switch provider {
switch provider { // nolint:exhaustive
case "mux":
return NewMuxDestination(obj)
case "youtube":
return NewYoutubeDestination(obj)
}

return nil
}

func GetDestinationProvider(destination interface{}) DestinationProviderType {
switch reflect.TypeOf(destination).String() {
case "*openapi.MuxDestination":
return MuxDestinationType
case "*openapi.YoutubeDestination":
return YoutubeDestinationType
default:
return UnknownDestinationType
}
}

// AssertDestinationsRequired checks if the required fields are not zero-ed
func AssertDestinationsRequired(obj interface{}) error {
switch GetDestinationProvider(obj) { // nolint:exhaustive
case YoutubeDestinationType:
return AssertYoutubeDestinationRequired(obj.(*YoutubeDestination))
case MuxDestinationType:
return AssertMuxDestinationRequired(obj.(*MuxDestination))
}
return nil
}
1 change: 1 addition & 0 deletions go/model_ffmpeg.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,6 @@ type FFMPEGCommand interface {
ToFFMPEG(*RenderQueue, *ProcessingQueue) error
GetOutputName() string
GetDuration() float32
HasYoutubeDestination() error
OverlayAllTracks([]string) string
}
8 changes: 4 additions & 4 deletions go/model_mux_destination.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,20 +35,20 @@ type MuxDestination struct {
Options MuxDestinationOptions `json:"options,omitempty"`
}

func NewMuxDestination(obj map[string]interface{}) interface{} {
func NewMuxDestination(obj map[string]interface{}) *MuxDestination {
destination := &MuxDestination{
Provider: obj["provider"].(string),
}

if obj["options"] != nil {
destination.Options = obj["options"].(MuxDestinationOptions)
destination.Options = *NewMuxDestinationOptions(obj["options"].(map[string]interface{}))
}

return destination
}

// AssertMuxDestinationRequired checks if the required fields are not zero-ed
func AssertMuxDestinationRequired(obj MuxDestination) error {
func AssertMuxDestinationRequired(obj *MuxDestination) error {
elements := map[string]interface{}{
"provider": obj.Provider,
}
Expand All @@ -72,6 +72,6 @@ func AssertRecurseMuxDestinationRequired(objSlice interface{}) error {
if !ok {
return ErrTypeAssertionError
}
return AssertMuxDestinationRequired(aMuxDestination)
return AssertMuxDestinationRequired(&aMuxDestination)
})
}
13 changes: 13 additions & 0 deletions go/model_mux_destination_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,19 @@ type MuxDestinationOptions struct {
PlaybackPolicy []string `json:"playbackPolicy,omitempty"`
}

func NewMuxDestinationOptions(obj map[string]interface{}) *MuxDestinationOptions {
options := &MuxDestinationOptions{}

if obj["playbackPolicy"] != nil {
policy := obj["playbackPolicy"].([]interface{})
for _, i := range policy {
options.PlaybackPolicy = append(options.PlaybackPolicy, i.(string))
}
}

return options
}

// AssertMuxDestinationOptionsRequired checks if the required fields are not zero-ed
func AssertMuxDestinationOptionsRequired(obj MuxDestinationOptions) error {
return nil
Expand Down
33 changes: 19 additions & 14 deletions go/model_output.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ type Output struct {
Destinations []interface{} `json:"destinations,omitempty"`
}

func NewOutput(data map[string]interface{}) *Output {
func NewOutput(data map[string]interface{}, destinations []interface{}) *Output {
output := &Output{
Format: data["format"].(string),
}
Expand Down Expand Up @@ -106,13 +106,7 @@ func NewOutput(data map[string]interface{}) *Output {
*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)
}
}
output.Destinations = destinations
return output
}

Expand All @@ -123,7 +117,16 @@ func (s *Output) UnmarshalJSON(data []byte) error {
return err
}

*s = *NewOutput(obj)
var destinations []interface{}
if obj["destinations"] != nil {
for _, dest := range obj["destinations"].([]interface{}) {
var provider = dest.(map[string]interface{})["provider"].(string)
destination := NewDestination(provider, dest.(map[string]interface{}))
destinations = append(destinations, destination)
}
}

*s = *NewOutput(obj, destinations)

return nil
}
Expand Down Expand Up @@ -193,11 +196,13 @@ 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
// }
// }
if obj.Destinations != nil {
for i := range obj.Destinations {
if err := AssertDestinationsRequired(obj.Destinations[i]); err != nil {
return err
}
}
}
return nil
}

Expand Down
Loading