blob: bf8c40849bdf23c897dde39a3b9cfb6f91b5fa42 [file] [log] [blame]
// 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/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/lib/xrpc"
_ "v.io/x/ref/runtime/factories/static"
)
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 := xrpc.NewServer(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)
}