| // Copyright 2015 The Vanadium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package main |
| |
| import ( |
| "bufio" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "net/http" |
| "os" |
| "os/exec" |
| "strings" |
| "sync" |
| |
| "v.io/v23" |
| "v.io/v23/context" |
| "v.io/v23/rpc" |
| "v.io/v23/security" |
| "v.io/x/lib/cmdline" |
| "v.io/x/lib/vlog" |
| "v.io/x/media_sharing" |
| "v.io/x/ref/lib/signals" |
| "v.io/x/ref/lib/v23cmd" |
| _ "v.io/x/ref/runtime/factories/roaming" |
| ) |
| |
| func main() { |
| // When we run under the device manager, no environment is set up. |
| // We want to have some kind of path. |
| if os.Getenv("PATH") == "" { |
| os.Setenv("PATH", "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin") |
| os.Setenv("HOME", "/home/testuser1") |
| } |
| |
| cmdline.Main(root) |
| } |
| |
| var root = &cmdline.Command{ |
| Name: os.Args[0], |
| ArgsName: "<name>", |
| ArgsLong: "<name> Is the name to mount the server under.", |
| Runner: v23cmd.RunnerFunc(serve), |
| Short: "Serve a display to remote clients.", |
| } |
| |
| func serve(ctx *context.T, env *cmdline.Env, args []string) error { |
| name := "" |
| if len(args) > 0 { |
| name = args[0] |
| fmt.Printf("mounting under: %s\n", name) |
| } |
| |
| _, server, err := v23.WithNewServer(ctx, name, defaultMediaServer(), security.AllowEveryone()) |
| if err != nil { |
| return err |
| } |
| fmt.Printf("Listening at: %s", server.Status().Endpoints[0].Name()) |
| |
| <-signals.ShutdownOnSignals(ctx) |
| return nil |
| } |
| |
| // handler defines the interface for a media handler. |
| type handler interface { |
| // Matches returns true if this handler supports mimetype. |
| Matches(mimetype string) bool |
| |
| // Display starts displaying the content in r. |
| // This method should not be blocking and shuould return quickly. |
| // The returned function should be called to completely stop and clean up |
| // the media playback before starting a new display. |
| Display(ctx *context.T, mimetype string, r io.ReadCloser) (func(), error) |
| } |
| |
| type eogHandler struct{} |
| |
| func (eogHandler) Matches(mimetype string) bool { |
| return strings.HasPrefix(mimetype, "image/") |
| } |
| |
| func (eogHandler) Display(ctx *context.T, mimetype string, r io.ReadCloser) (func(), error) { |
| // eog cannot read from a pipe, so we have to write the file to |
| // the filesystem before displaying it. |
| defer r.Close() |
| tmp, err := ioutil.TempFile("", "") |
| if err != nil { |
| return nil, err |
| } |
| if _, err := io.Copy(tmp, r); err != nil { |
| os.Remove(tmp.Name()) |
| return nil, err |
| } |
| tmp.Close() |
| |
| cmd := exec.Command("eog", "--display", ":0", "-f", tmp.Name()) |
| cmd.Stdout = os.Stdout |
| cmd.Stderr = os.Stderr |
| stop := func() { |
| if err := cmd.Process.Kill(); err != nil { |
| vlog.Errorf("Could not kill eog: %v", err) |
| } |
| cmd.Wait() |
| os.Remove(tmp.Name()) |
| } |
| if err := cmd.Start(); err != nil { |
| return stop, err |
| } |
| return stop, nil |
| } |
| |
| type omxHandler struct{} |
| |
| func (omxHandler) Matches(mimetype string) bool { |
| return strings.HasPrefix(mimetype, "video") |
| } |
| |
| func (omxHandler) Display(ctx *context.T, mimetype string, r io.ReadCloser) (func(), error) { |
| defer r.Close() |
| tmp, err := ioutil.TempFile("", "") |
| if err != nil { |
| return nil, err |
| } |
| if _, err := io.Copy(tmp, r); err != nil { |
| os.Remove(tmp.Name()) |
| return nil, err |
| } |
| tmp.Close() |
| |
| args := []string{ |
| "-b", |
| tmp.Name(), |
| } |
| |
| vlog.Infof("Running: omxplayer %s", strings.Join(args, " ")) |
| |
| cmd := exec.Command("omxplayer", args...) |
| cmd.Stdin = r |
| cmd.Stdout = os.Stdout |
| cmd.Stderr = os.Stderr |
| if err := cmd.Start(); err != nil { |
| return nil, err |
| } |
| return func() { |
| if err := cmd.Process.Kill(); err != nil { |
| vlog.Errorf("Could not kill omx: %v", err) |
| } |
| cmd.Wait() |
| os.Remove(tmp.Name()) |
| }, nil |
| } |
| |
| type vlcHandler struct{} |
| |
| func (vlcHandler) Matches(mimetype string) bool { |
| return strings.HasPrefix(mimetype, "audio/") || strings.HasPrefix(mimetype, "video/") |
| } |
| |
| func (vlcHandler) Display(ctx *context.T, mimetype string, r io.ReadCloser) (func(), error) { |
| defer r.Close() |
| tmp, err := ioutil.TempFile("", "") |
| if err != nil { |
| return nil, err |
| } |
| if _, err := io.Copy(tmp, r); err != nil { |
| os.Remove(tmp.Name()) |
| return nil, err |
| } |
| tmp.Close() |
| |
| args := []string{ |
| "--no-video-title-show", |
| "--fullscreen", |
| "--x11-display", ":0", |
| tmp.Name(), |
| "vlc://quit", |
| } |
| |
| if strings.HasPrefix(mimetype, "audio/") { |
| args = append([]string{"--audio-visual=visual"}, args...) |
| } |
| |
| vlog.Infof("Running: vlc %s", strings.Join(args, " ")) |
| |
| cmd := exec.Command("vlc", args...) |
| cmd.Stdout = os.Stdout |
| cmd.Stderr = os.Stderr |
| if err := cmd.Start(); err != nil { |
| return nil, err |
| } |
| return func() { |
| if err := cmd.Process.Kill(); err != nil { |
| vlog.Errorf("Could not kill vlc: %v", err) |
| } |
| cmd.Wait() |
| os.Remove(tmp.Name()) |
| }, nil |
| } |
| |
| type media struct { |
| handlers []handler |
| |
| stopMu sync.Mutex |
| stop func() |
| } |
| |
| func defaultMediaServer() media_sharing.MediaSharingServerStub { |
| m := &media{ |
| handlers: []handler{ |
| &eogHandler{}, |
| &omxHandler{}, |
| &vlcHandler{}, |
| }, |
| } |
| return media_sharing.MediaSharingServer(m) |
| } |
| |
| func detectMimeType(r *bufio.Reader) string { |
| // If the server didn't tell us the content type, we will have to guess. |
| // Note that I am purposefully ignoring the error from Peek. The guesser looks |
| // at up to 512 bytes. We just get as many as we can and make our best |
| // guess with that. |
| data, _ := r.Peek(512) |
| return http.DetectContentType(data) |
| } |
| |
| type bufferedReadCloser struct { |
| *bufio.Reader |
| close func() error |
| } |
| |
| func (bc *bufferedReadCloser) Close() error { |
| return bc.close() |
| } |
| |
| func (m *media) findHandler(mimetype string) (handler, error) { |
| for _, h := range m.handlers { |
| if h.Matches(mimetype) { |
| return h, nil |
| } |
| } |
| return nil, fmt.Errorf("Unsupported content type: %s", mimetype) |
| } |
| |
| func (m *media) display(ctx *context.T, mimetype string, r io.ReadCloser) error { |
| rc := &bufferedReadCloser{ |
| Reader: bufio.NewReader(r), |
| close: r.Close, |
| } |
| |
| if mimetype == "" { |
| mimetype = detectMimeType(rc.Reader) |
| } |
| |
| // Find a suitable handler. |
| h, err := m.findHandler(mimetype) |
| if err != nil { |
| return err |
| } |
| |
| m.stopMu.Lock() |
| if m.stop != nil { |
| m.stop() |
| } |
| m.stop, err = h.Display(ctx, mimetype, r) |
| m.stopMu.Unlock() |
| return err |
| } |
| |
| // DisplayURL will cause the server to display whatever media is at |
| // the given URL. The server will rely on the ContentType response |
| // header it gets when fetching the url to decide how to display |
| // the media. |
| func (m *media) DisplayUrl(ctx *context.T, call rpc.ServerCall, url string) error { |
| resp, err := http.Get(url) |
| if err != nil { |
| return err |
| } |
| return m.display(ctx, resp.Header.Get("Content-Type"), resp.Body) |
| } |
| |
| // The streamReader adapts a vanadium steam of byte slices to an io.ReadCloser. |
| type streamReader struct { |
| last []byte |
| stream interface { |
| Advance() bool |
| Value() []byte |
| Err() error |
| } |
| } |
| |
| func (s *streamReader) Read(p []byte) (int, error) { |
| if len(s.last) == 0 { |
| if !s.stream.Advance() { |
| err := s.stream.Err() |
| if err == nil { |
| err = io.EOF |
| } |
| return 0, err |
| } |
| s.last = s.stream.Value() |
| } |
| if len(s.last) == 0 { |
| return 0, nil |
| } |
| n := copy(p, s.last) |
| s.last = s.last[n:] |
| return n, nil |
| } |
| |
| func (s *streamReader) Close() error { |
| // Do nothing. |
| return nil |
| } |
| |
| // DisplayBytes will cause the server to display whatever media is |
| // sent in the stream. In the case of audio or movie media, the |
| // media should be played while the data is streaming. The mediaType |
| // can be used by the server to decide how to display the media. |
| func (m *media) DisplayBytes(ctx *context.T, call media_sharing.MediaSharingDisplayBytesServerCall, mediaType string) error { |
| r := &streamReader{stream: call.RecvStream()} |
| return m.display(ctx, mediaType, r) |
| } |