package server import ( "context" "io" "log" "net/http" "os" "time" pbMediaSet "git.netflux.io/rob/clipper/generated/pb/media_set" "git.netflux.io/rob/clipper/media" "git.netflux.io/rob/clipper/youtube" "github.com/improbable-eng/grpc-web/go/grpcweb" "google.golang.org/grpc" "google.golang.org/grpc/grpclog" "google.golang.org/protobuf/types/known/durationpb" ) type Options struct { BindAddr string Timeout time.Duration YoutubeClient youtube.YoutubeClient S3Client media.S3Client } const ( fetchAudioTimeout = time.Minute * 5 ) // fetchMediaSetServiceController implements gRPC controller for FetchMediaSetService type fetchMediaSetServiceController struct { pbMediaSet.UnimplementedFetchServiceServer fetchMediaSetService *media.FetchMediaSetService } // Fetch fetches a pbMediaSet.MediaSet func (c *fetchMediaSetServiceController) Fetch(ctx context.Context, request *pbMediaSet.FetchRequest) (*pbMediaSet.MediaSet, error) { mediaSet, err := c.fetchMediaSetService.Fetch(ctx, request.GetId()) if err != nil { return nil, err } result := pbMediaSet.MediaSet{ Id: mediaSet.ID, Audio: &pbMediaSet.MediaSet_Audio{ Bytes: mediaSet.Audio.Bytes, Channels: int32(mediaSet.Audio.Channels), Frames: mediaSet.Audio.Frames, SampleRate: int32(mediaSet.Audio.SampleRate), }, Video: &pbMediaSet.MediaSet_Video{ Bytes: mediaSet.Video.Bytes, Duration: durationpb.New(mediaSet.Video.Duration), ThumbnailWidth: int32(mediaSet.Video.ThumbnailWidth), ThumbnailHeight: int32(mediaSet.Video.ThumbnailHeight), }, } return &result, nil } // TODO: wrap errors func (c *fetchMediaSetServiceController) FetchAudio(request *pbMediaSet.FetchAudioRequest, stream pbMediaSet.FetchService_FetchAudioServer) error { ctx, cancel := context.WithTimeout(context.Background(), fetchAudioTimeout) defer cancel() reader, err := c.fetchMediaSetService.FetchAudio(ctx, request.GetId(), int(request.GetNumBins())) if err != nil { return err } for { progress, err := reader.Read() if err != nil { if err == io.EOF { break } return err } // TODO: consider using int32 throughout the backend flow to avoid this. peaks := make([]int32, len(progress.Peaks)) for i, p := range progress.Peaks { peaks[i] = int32(p) } progressPb := pbMediaSet.FetchAudioProgress{ PercentCompleted: progress.PercentComplete, Peaks: peaks, } stream.Send(&progressPb) } return nil } func Start(options Options) error { grpcServer := grpc.NewServer() fetchMediaSetService := media.NewFetchMediaSetService(options.YoutubeClient, options.S3Client) pbMediaSet.RegisterFetchServiceServer(grpcServer, &fetchMediaSetServiceController{fetchMediaSetService: fetchMediaSetService}) grpclog.SetLogger(log.New(os.Stdout, "server: ", log.LstdFlags)) // TODO: proper CORS support grpcWebServer := grpcweb.WrapServer(grpcServer, grpcweb.WithOriginFunc(func(string) bool { return true })) handler := func(w http.ResponseWriter, r *http.Request) { grpcWebServer.ServeHTTP(w, r) } httpServer := http.Server{ Addr: options.BindAddr, ReadTimeout: options.Timeout, WriteTimeout: options.Timeout, Handler: http.HandlerFunc(handler), } return httpServer.ListenAndServe() }