Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add videosink display driver #27

Merged
merged 1 commit into from
Dec 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions videosink/display.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Copyright 2021 The Periph Authors. All rights reserved.
// Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file.

// Package videosink provides a display driver implementing an HTTP request
// handler. Client requests get an initial snapshot of the graphics buffer and
// are updated further on every change.
//
// The primary use case is the development of display outputs on a host
// machine. Additionally devices with network connectivity can use this driver
// to provide a copy of their local display via a web interface.
//
// The protocol used is "MJPEG" (https://en.wikipedia.org/wiki/Motion_JPEG)
// which is often used by IP cameras. Because of its better suitability for
// computer-drawn graphics the PNG image format is used by default. JPEG as
// a format can be selected via Options.Format or using the "format" URL
// parameter.
package videosink

import (
"image"
"image/color"
"image/draw"
"net/http"
"sync"

"periph.io/x/conn/v3/display"
)

// Options for videosink devices.
type Options struct {
// Width and height of the image buffer.
Width, Height int

// Format specifies the image format to send to clients.
Format ImageFormat

// TODO: Add options for JPEG and PNG encoder settings
}

type Display struct {
maruel marked this conversation as resolved.
Show resolved Hide resolved
defaultFormat ImageFormat

mu sync.Mutex
buffer *image.RGBA
clients map[*client]struct{}
snapshot map[imageConfig][]byte
}

var _ display.Drawer = (*Display)(nil)
var _ http.Handler = (*Display)(nil)

// New creates a new videosink device instance.
func New(opt *Options) *Display {
buffer := image.NewRGBA(image.Rect(0, 0, opt.Width, opt.Height))

// By default the alpha channel is set to full transparency. The following
// draw operation makes it opaque.
draw.Draw(buffer, buffer.Bounds(), image.Black, image.Point{}, draw.Src)
maruel marked this conversation as resolved.
Show resolved Hide resolved

return &Display{
buffer: buffer,
clients: map[*client]struct{}{},
snapshot: map[imageConfig][]byte{},
defaultFormat: opt.Format,
}
}

// String returns the name of the device.
func (d *Display) String() string {
return "VideoSink"
}

// Halt implements conn.Resource and terminates all running client requests
// asynchronously.
func (d *Display) Halt() error {
d.mu.Lock()
d.terminateClientsLocked()
d.mu.Unlock()

return nil
}

// ColorModel implements display.Drawer.
func (d *Display) ColorModel() color.Model {
return d.buffer.ColorModel()
}

// Bounds implements display.Drawer.
func (d *Display) Bounds() image.Rectangle {
return d.buffer.Bounds()
}

// Draw implements display.Drawer.
func (d *Display) Draw(dstRect image.Rectangle, src image.Image, srcPts image.Point) error {
d.mu.Lock()
draw.Draw(d.buffer, dstRect, src, srcPts, draw.Src)
d.bufferChangedLocked()
d.mu.Unlock()

return nil
}
15 changes: 15 additions & 0 deletions videosink/display_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright 2021 The Periph Authors. All rights reserved.
// Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file.

package videosink

import "testing"

func TestNewHalt(t *testing.T) {
d := New(&Options{Width: 100, Height: 100})

if err := d.Halt(); err != nil {
t.Errorf("Halt() failed: %v", err)
}
}
46 changes: 46 additions & 0 deletions videosink/encoder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright 2021 The Periph Authors. All rights reserved.
// Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file.

package videosink

import (
"image/jpeg"
"image/png"
"sync"
)

var jpegOptions = jpeg.Options{
Quality: 95,
}

type pngEncoderBufferPool sync.Pool

func (p *pngEncoderBufferPool) Get() *png.EncoderBuffer {
buf, _ := (*sync.Pool)(p).Get().(*png.EncoderBuffer)
return buf
}

func (p *pngEncoderBufferPool) Put(buf *png.EncoderBuffer) {
(*sync.Pool)(p).Put(buf)
}

type pngEncoderManager struct {
once sync.Once
pool pngEncoderBufferPool
enc *png.Encoder
}

var pngEncoder pngEncoderManager

// get returns a PNG encoder with a globally shared buffer pool.
func (m *pngEncoderManager) get() *png.Encoder {
m.once.Do(func() {
m.enc = &png.Encoder{
CompressionLevel: png.BestSpeed,
hansmi marked this conversation as resolved.
Show resolved Hide resolved
BufferPool: &m.pool,
}
})

return m.enc
}
53 changes: 53 additions & 0 deletions videosink/format.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright 2021 The Periph Authors. All rights reserved.
// Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file.

package videosink

import "fmt"

type ImageFormat int

const (
PNG ImageFormat = iota
JPEG

// DefaultFormat is the format used when not set explicitly in options or
// as a URL parameter.
DefaultFormat = PNG
)

func (f ImageFormat) String() string {
hansmi marked this conversation as resolved.
Show resolved Hide resolved
switch f {
case PNG:
return "PNG"
case JPEG:
return "JPEG"
default:
return fmt.Sprint(int(f))
}
}

func (f ImageFormat) mimeType() string {
switch f {
case PNG:
return "image/png"
case JPEG:
return "image/jpeg"
}

return "application/octet-stream"
}

// ImageFormatFromString returns the ImageFormat value for the given format
// abbreviation.
func ImageFormatFromString(value string) (ImageFormat, error) {
switch value {
case "png":
return PNG, nil
case "jpg", "jpeg":
return JPEG, nil
}

return DefaultFormat, fmt.Errorf("unrecognized image format %q", value)
}
53 changes: 53 additions & 0 deletions videosink/format_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright 2021 The Periph Authors. All rights reserved.
// Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file.

package videosink

import (
"fmt"
"testing"
)

func TestImageFormat(t *testing.T) {
for _, tc := range []struct {
format ImageFormat
wantString string
wantMimeType string
}{
{
format: ImageFormat(-1),
wantString: "-1",
wantMimeType: "application/octet-stream",
},
{
wantString: "PNG",
wantMimeType: "image/png",
},
{
format: DefaultFormat,
wantString: "PNG",
wantMimeType: "image/png",
},
{
format: PNG,
wantString: "PNG",
wantMimeType: "image/png",
},
{
format: JPEG,
wantString: "JPEG",
wantMimeType: "image/jpeg",
},
} {
t.Run(fmt.Sprint(tc), func(t *testing.T) {
if got := tc.format.String(); got != tc.wantString {
t.Errorf("String() returned %q, want %q", got, tc.wantString)
}

if got := tc.format.mimeType(); got != tc.wantMimeType {
t.Errorf("mimeType() returned %q, want %q", got, tc.wantMimeType)
}
})
}
}
Loading