exp.upspin.io@v0.0.0-20230625230448-5076e5b595ec/cmd/camclient/main.go (about)

     1  // Copyright 2017 The Upspin Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Command camclient serves a video stream generated by
     6  // exp.upspin.io/cmd/camserver over HTTP as a Motion JPEG stream.
     7  package main // import "exp.upspin.io/cmd/camclient"
     8  
     9  import (
    10  	"flag"
    11  	"fmt"
    12  	"mime/multipart"
    13  	"net/http"
    14  	"net/textproto"
    15  	"os"
    16  	"sync"
    17  	"time"
    18  
    19  	"upspin.io/client"
    20  	"upspin.io/client/clientutil"
    21  	"upspin.io/config"
    22  	"upspin.io/flags"
    23  	"upspin.io/log"
    24  	"upspin.io/transports"
    25  	"upspin.io/upspin"
    26  )
    27  
    28  func main() {
    29  	flag.Usage = func() {
    30  		fmt.Fprintln(os.Stderr, "usage: camclient [flags] <Upspin path>")
    31  		flag.PrintDefaults()
    32  	}
    33  
    34  	flags.Parse(flags.Client, "http")
    35  
    36  	if flag.NArg() != 1 {
    37  		fmt.Fprintln(os.Stderr, "camclient: need exactly one path name argument")
    38  		flag.Usage()
    39  		os.Exit(2)
    40  	}
    41  	name := upspin.PathName(flag.Arg(0))
    42  
    43  	cfg, err := config.FromFile(flags.Config)
    44  	if err != nil {
    45  		log.Fatal(err)
    46  	}
    47  	transports.Init(cfg)
    48  
    49  	h, err := newHandler(cfg, name)
    50  	if err != nil {
    51  		log.Fatal(err)
    52  	}
    53  	http.Handle("/", h)
    54  	log.Fatal(http.ListenAndServe(flags.HTTPAddr, nil))
    55  }
    56  
    57  // handler is an http.Handler that serves a Motion JPEG of a camserver stream.
    58  type handler struct {
    59  	mu     sync.Mutex
    60  	update *sync.Cond
    61  	frame  []byte
    62  }
    63  
    64  // newHandler initializes a handler that streams the provided camserver path
    65  // with the given config. The handler only watches and fetches each frame once
    66  // regardless of the number of concurrent viewers.
    67  func newHandler(cfg upspin.Config, name upspin.PathName) (http.Handler, error) {
    68  	h := &handler{}
    69  	h.update = sync.NewCond(&h.mu)
    70  
    71  	c := client.New(cfg)
    72  	dir, err := c.DirServer(name)
    73  	if err != nil {
    74  		return nil, err
    75  	}
    76  	entries := make(chan *upspin.DirEntry)
    77  	go func() {
    78  		for e := range entries {
    79  			// Read the latest frame.
    80  			frame, err := clientutil.ReadAll(cfg, e)
    81  			if err != nil {
    82  				log.Error.Print(err)
    83  				continue
    84  			}
    85  			// Share it with viewers.
    86  			h.mu.Lock()
    87  			h.frame = frame
    88  			h.mu.Unlock()
    89  			h.update.Broadcast()
    90  		}
    91  	}()
    92  	go func() {
    93  		var (
    94  			fetched, skipped int
    95  			lastUpdate       = time.Now()
    96  			done             chan struct{}
    97  		)
    98  		for {
    99  			if done != nil {
   100  				close(done)
   101  			}
   102  			done = make(chan struct{})
   103  			events, err := dir.Watch(name, 0, done)
   104  			if err != nil {
   105  				log.Error.Print(err)
   106  				time.Sleep(5 * time.Second)
   107  				continue
   108  			}
   109  			for e := range events {
   110  				if e.Error != nil {
   111  					log.Error.Print(e.Error)
   112  					break
   113  				}
   114  				// Do a non-blocking send here so that we skip
   115  				// this frame if we're still fetching an old
   116  				// frame, to prevent us from failling behind.
   117  				select {
   118  				case entries <- e.Entry:
   119  					log.Debug.Printf("fetching frame %d", e.Entry.Sequence)
   120  					fetched++
   121  				default:
   122  					log.Debug.Printf("skipped frame %d", e.Entry.Sequence)
   123  					skipped++
   124  				}
   125  				if d := time.Since(lastUpdate); d > 10*time.Second {
   126  					sec := float64(d) / float64(time.Second)
   127  					fps := float64(fetched) / sec
   128  					sps := float64(skipped) / sec
   129  					log.Info.Printf("frames per second: %.3g fetched, %.3g skipped", fps, sps)
   130  					fetched, skipped, lastUpdate = 0, 0, time.Now()
   131  				}
   132  			}
   133  		}
   134  	}()
   135  
   136  	return h, nil
   137  }
   138  
   139  func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
   140  	// Motion JPEG is a mulitpart MIME-encoded series of JPEG images.
   141  	mw := multipart.NewWriter(w)
   142  	w.Header().Set("Content-Type", "multipart/x-mixed-replace;boundary="+mw.Boundary())
   143  	partHeader := textproto.MIMEHeader{"Content-Type": {"image/jpeg"}}
   144  	for {
   145  		// Wait for a new frame to become available.
   146  		h.mu.Lock()
   147  		h.update.Wait()
   148  		frame := h.frame
   149  		h.mu.Unlock()
   150  		// Write that frame as a new MIME part.
   151  		w, err := mw.CreatePart(partHeader)
   152  		if err != nil {
   153  			log.Println(err)
   154  			return
   155  		}
   156  		_, err = w.Write(frame)
   157  		if err != nil {
   158  			log.Println(err)
   159  			return
   160  		}
   161  	}
   162  }