media-sharing: Add an initial server implementation.

Change-Id: I25f41c27965d8933127c332f9e53a37cdd7a8dc3
diff --git a/go/src/v.io/x/media_sharing/media_sharing.go b/go/src/v.io/x/media_sharing/media_sharing.go
deleted file mode 100644
index 3fbd6ee..0000000
--- a/go/src/v.io/x/media_sharing/media_sharing.go
+++ /dev/null
@@ -1,13 +0,0 @@
-// 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 media_sharing
-
-import (
-	"fmt"
-)
-
-func main() {
-	fmt.Printf("hello\n")
-}
diff --git a/go/src/v.io/x/media_sharing/mediaserver/mediaserver.go b/go/src/v.io/x/media_sharing/mediaserver/mediaserver.go
index 7d48c4c..3b4e17d 100644
--- a/go/src/v.io/x/media_sharing/mediaserver/mediaserver.go
+++ b/go/src/v.io/x/media_sharing/mediaserver/mediaserver.go
@@ -5,13 +5,22 @@
 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"
@@ -34,6 +43,7 @@
 	name := ""
 	if len(args) > 0 {
 		name = args[0]
+		fmt.Printf("mounting under: %s\n", name)
 	}
 
 	server, err := v23.NewServer(ctx)
@@ -44,7 +54,7 @@
 	if err != nil {
 		return err
 	}
-	if err := server.Serve(name, media_sharing.MediaSharingServer(&media{}), nil); err != nil {
+	if err := server.Serve(name, defaultMediaServer(), security.AllowEveryone()); err != nil {
 		return err
 	}
 	fmt.Printf("Listening at: %s", eps[0].Name())
@@ -53,13 +63,194 @@
 	return nil
 }
 
-type media struct{}
+// 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 {
+		return nil, err
+	}
+
+	cmd := exec.Command("eog", "-f", tmp.Name())
+	stop := func() {
+		if cmd.Process != nil {
+			if err := cmd.Process.Kill(); err != nil {
+				vlog.Errorf("Could not kill eog: %v", err)
+			}
+		}
+		os.Remove(tmp.Name())
+	}
+	if err := cmd.Start(); err != nil {
+		return stop, err
+	}
+	return stop, 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) {
+	args := []string{
+		"--no-video-title-show",
+		"--fullscreen",
+		"-",
+		"vlc://quit",
+	}
+
+	if strings.HasPrefix(mimetype, "audio/") {
+		args = append([]string{"--audio-visual=visual"}, args...)
+	}
+
+	cmd := exec.Command("vlc", args...)
+	cmd.Stdin = r
+	if err := cmd.Start(); err != nil {
+		return nil, err
+	}
+	return func() {
+		r.Close()
+		if err := cmd.Process.Kill(); err != nil {
+			vlog.Errorf("Could not kill vlc: %v", err)
+		}
+	}, nil
+}
+
+type media struct {
+	handlers []handler
+
+	stopMu sync.Mutex
+	stop   func()
+}
+
+func defaultMediaServer() media_sharing.MediaSharingServerStub {
+	m := &media{
+		handlers: []handler{
+			&eogHandler{},
+			&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
 }
 
@@ -68,5 +259,6 @@
 // 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 {
-	return nil
+	r := &streamReader{stream: call.RecvStream()}
+	return m.display(ctx, mediaType, r)
 }