replicas-computer 0.3.7 → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -4
- package/assets/desktop/replicas-chrome +7 -0
- package/dist/index.js +509 -423
- package/dist/stream-viewer.js +17 -2
- package/native/go.mod +3 -0
- package/native/internal/capture/fixture.go +130 -0
- package/native/internal/capture/native.go +462 -0
- package/native/internal/capture/native.h +31 -0
- package/native/internal/capture/native_test.go +22 -0
- package/native/internal/capture/options.go +7 -0
- package/native/internal/capture/yuv.go +55 -0
- package/native/internal/capture/yuv_fallback.go +23 -0
- package/native/main.go +105 -0
- package/native/options.go +52 -0
- package/package.json +10 -3
- package/patches/libyuv-chroma-rounding.patch +83 -0
- package/scripts/build-capture.sh +30 -0
package/dist/stream-viewer.js
CHANGED
|
@@ -736,7 +736,7 @@ var updateDiagnostics = (stats) => {
|
|
|
736
736
|
css: `${Math.round(bounds.width)}\xD7${Math.round(bounds.height)}`,
|
|
737
737
|
physical: `${Math.round(bounds.width * devicePixelRatio)}\xD7${Math.round(bounds.height * devicePixelRatio)} @${devicePixelRatio}x`
|
|
738
738
|
};
|
|
739
|
-
Object.assign(state, { transport, transportReason, socketRttMs: socketRtt, view, route, dims, ...stats });
|
|
739
|
+
Object.assign(state, { transport, transportReason, socketRttMs: socketRtt, view, route, dims, decodeMs: stats.decodeMs, receiverProcessingMs: stats.receiverProcessingMs, ...stats });
|
|
740
740
|
if (!showDiagnostics) return;
|
|
741
741
|
const network = stats.lossPercent === void 0 ? `${stats.decodeQueue} queued` : `${stats.lossPercent.toFixed(1)}% loss \xB7 ${(stats.playbackDroppedPercent ?? 0).toFixed(1)}% playback drops \xB7 ${stats.nacks ?? 0} nack \xB7 ${stats.freezes ?? 0} freezes \xB7 ${(stats.jitterBufferMs ?? 0).toFixed(0)} ms buffer`;
|
|
742
742
|
const encoder = telemetry?.encoder;
|
|
@@ -745,6 +745,7 @@ var updateDiagnostics = (stats) => {
|
|
|
745
745
|
`${transport ?? "connecting"}${transportReason ? ` (${transportReason})` : ""} \xB7 ${route.path || "\u2014"} \xB7 ${route.codec || config?.videoCodec || ""} \xB7 ${view} view`,
|
|
746
746
|
`source ${dims.source} \xB7 decoded ${dims.decoded || "\u2014"} \xB7 css ${dims.css} \xB7 device ${dims.physical}`,
|
|
747
747
|
`${route.displayedFps.toFixed(1)} displayed/s \xB7 ${stats.videoFps.toFixed(1)} decoded/s \xB7 ${route.kbps.toFixed(0)} kbps \xB7 ${stats.rttMs.toFixed(0)} ms rtt \xB7 ${socketRtt.toFixed(0)} ms via proxy \xB7 ${network}`,
|
|
748
|
+
transport === "webrtc" ? `receiver: ${stats.decodeMs?.toFixed(2) ?? "\u2014"} ms decode \xB7 ${stats.receiverProcessingMs?.toFixed(2) ?? "\u2014"} ms receive \u2192 decoded (includes buffer + decode; excludes display)` : "",
|
|
748
749
|
encoder ? `encoder ${encoder.profile} crf ${encoder.crf} \xB7 ${encoder.codecProfile} ${encoder.pixelFormat} ${encoder.color} \xB7 rung ${encoder.rung}/${encoder.rungs - 1} cap ${encoder.kbpsCap} kbps${encoder.adaptive ? "" : " pinned"} \xB7 keyframe every ${encoder.keyframeMs} ms \xB7 ${encoder.fps} fps ${encoder.kbps} kbps \xB7 ${encoder.preset}` : "",
|
|
749
750
|
telemetry && server ? `server cpu: encoder ${telemetry.cpu.encoderPercent}% \xB7 stream ${telemetry.cpu.serverPercent}% \xB7 load ${telemetry.cpu.load} on ${telemetry.cpu.cores} cores \xB7 budget ${server.budgetKbps} kbps sent ${server.sentKbps} \xB7 queue ${server.queueMs} ms ${server.queuedPackets} pkt \xB7 stale ${server.staleEvents} dropped ${server.droppedFrames} \xB7 repairs ${server.repairsSent}/${server.repairsSkipped} skipped \xB7 copies ${server.duplicatesSent}${server.protecting ? " on" : ""} \xB7 remb ${server.rembKbps ?? "\u2014"}` : "",
|
|
750
751
|
transport === "websocket" && server ? `socket: ${Math.round(server.bufferedBytes / 1024)} KiB unsent \xB7 ${server.withheldFrames} deltas withheld${server.waitingForKeyframe ? " \xB7 awaiting keyframe" : ""} \xB7 ${decoderResets} decoder resets` : "",
|
|
@@ -773,6 +774,9 @@ var reportWebRtcStats = async (pc, stats) => {
|
|
|
773
774
|
const now = performance.now();
|
|
774
775
|
const playback = screen.getVideoPlaybackQuality?.();
|
|
775
776
|
const counters = {
|
|
777
|
+
id: video?.id,
|
|
778
|
+
totalDecodeTime: video?.totalDecodeTime,
|
|
779
|
+
totalProcessingDelay: video?.totalProcessingDelay,
|
|
776
780
|
framesDecoded: video?.framesDecoded ?? 0,
|
|
777
781
|
totalVideoFrames: playback?.totalVideoFrames ?? 0,
|
|
778
782
|
droppedVideoFrames: playback?.droppedVideoFrames ?? 0,
|
|
@@ -790,6 +794,7 @@ var reportWebRtcStats = async (pc, stats) => {
|
|
|
790
794
|
clearTimeout(webrtcDeadline);
|
|
791
795
|
webrtcDeadline = void 0;
|
|
792
796
|
}
|
|
797
|
+
if (inbound && (inbound.id !== counters.id || counters.framesDecoded < inbound.framesDecoded)) inbound = void 0;
|
|
793
798
|
if (!inbound) inboundHistory = [];
|
|
794
799
|
while (inboundHistory.length > 1 && inboundHistory[1].at <= now - 1e3) inboundHistory.shift();
|
|
795
800
|
const oldest = inboundHistory[0];
|
|
@@ -803,7 +808,17 @@ var reportWebRtcStats = async (pc, stats) => {
|
|
|
803
808
|
const received = counters.packetsReceived - inbound.packetsReceived;
|
|
804
809
|
stats.videoPacketsPerSecond = Math.max(0, received) / seconds;
|
|
805
810
|
const emitted = counters.jitterBufferEmittedCount - inbound.jitterBufferEmittedCount;
|
|
806
|
-
|
|
811
|
+
const decoded = counters.framesDecoded - inbound.framesDecoded;
|
|
812
|
+
stats.videoFps = decoded / seconds;
|
|
813
|
+
if (decoded > 0) {
|
|
814
|
+
for (const [counter, metric] of [["totalDecodeTime", "decodeMs"], ["totalProcessingDelay", "receiverProcessingMs"]]) {
|
|
815
|
+
const current = counters[counter];
|
|
816
|
+
const previous = inbound[counter];
|
|
817
|
+
if (current !== void 0 && previous !== void 0 && Number.isFinite(current) && Number.isFinite(previous) && previous >= 0 && current >= previous) {
|
|
818
|
+
stats[metric] = (current - previous) * 1e3 / decoded;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
807
822
|
const total = counters.totalVideoFrames - inbound.totalVideoFrames;
|
|
808
823
|
const dropped = Math.max(0, counters.droppedVideoFrames - inbound.droppedVideoFrames);
|
|
809
824
|
if (playback) stats.playbackDroppedPercent = total > 0 ? dropped * 100 / total : 0;
|
package/native/go.mod
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
//go:build libyuv && capturetest
|
|
2
|
+
|
|
3
|
+
package capture
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
#define XUTIL_DEFINE_FUNCTIONS
|
|
7
|
+
#include <X11/Xlib.h>
|
|
8
|
+
#include <X11/Xutil.h>
|
|
9
|
+
#include <libavutil/frame.h>
|
|
10
|
+
#include <libyuv/convert.h>
|
|
11
|
+
#include <libyuv/convert_from_argb.h>
|
|
12
|
+
#include <libyuv/row.h>
|
|
13
|
+
*/
|
|
14
|
+
import "C"
|
|
15
|
+
|
|
16
|
+
import (
|
|
17
|
+
"bytes"
|
|
18
|
+
"errors"
|
|
19
|
+
"fmt"
|
|
20
|
+
"runtime"
|
|
21
|
+
"unsafe"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
// cgo cannot be imported directly from a _test.go file.
|
|
25
|
+
func checkPartialCapture(width, height int, i420, bt709 bool) error {
|
|
26
|
+
runtime.LockOSThread()
|
|
27
|
+
defer runtime.UnlockOSThread()
|
|
28
|
+
d, err := newDesktop(Options{Width: width, Height: height}, true)
|
|
29
|
+
if err != nil {
|
|
30
|
+
return err
|
|
31
|
+
}
|
|
32
|
+
defer d.close()
|
|
33
|
+
if d.attributes.width != 128 || d.attributes.height != 96 {
|
|
34
|
+
return errors.New("capture tests require the isolated 128x96 display from test:capture")
|
|
35
|
+
}
|
|
36
|
+
e := &encoder{}
|
|
37
|
+
defer e.close()
|
|
38
|
+
e.matrix, err = newColorMatrix(bt709)
|
|
39
|
+
if err != nil {
|
|
40
|
+
return err
|
|
41
|
+
}
|
|
42
|
+
e.frame = C.av_frame_alloc()
|
|
43
|
+
if e.frame == nil {
|
|
44
|
+
return errors.New("cannot allocate test frame")
|
|
45
|
+
}
|
|
46
|
+
e.frame.width, e.frame.height = C.int(width), C.int(height)
|
|
47
|
+
e.frame.format = C.AV_PIX_FMT_YUV444P
|
|
48
|
+
chroma := 0
|
|
49
|
+
if i420 {
|
|
50
|
+
e.frame.format = C.AV_PIX_FMT_YUV420P
|
|
51
|
+
chroma = 1
|
|
52
|
+
}
|
|
53
|
+
if err = avCheck(C.av_frame_get_buffer(e.frame, 32)); err != nil {
|
|
54
|
+
return err
|
|
55
|
+
}
|
|
56
|
+
s := Stream{desktop: d, encoder: e}
|
|
57
|
+
gc := C.XCreateGC(d.display, d.root, 0, nil)
|
|
58
|
+
defer C.XFreeGC(d.display, gc)
|
|
59
|
+
rectangles := [][4]int{
|
|
60
|
+
{0, 0, width, height},
|
|
61
|
+
{0, 0, 1, 1},
|
|
62
|
+
{width - 1, height - 1, 1, 1},
|
|
63
|
+
{width - 3, 1, 9, 3},
|
|
64
|
+
{1, height - 3, 3, 9},
|
|
65
|
+
{3, 5, 7, 11},
|
|
66
|
+
{width/2 + 1, height/2 + 1, 1, 1},
|
|
67
|
+
{0, 0, width, height},
|
|
68
|
+
}
|
|
69
|
+
size := width * height
|
|
70
|
+
reference := make([]byte, size*3)
|
|
71
|
+
strides := [3]C.int{C.int(width), C.int(width >> chroma), C.int(width >> chroma)}
|
|
72
|
+
partial, resized := 0, 0
|
|
73
|
+
previousWidth, previousHeight := width, height
|
|
74
|
+
for step := 0; step < 128; step++ {
|
|
75
|
+
r := rectangles[step%len(rectangles)]
|
|
76
|
+
C.XSetForeground(d.display, gc, C.ulong((step+1)*0x915b37&0xffffff))
|
|
77
|
+
C.XFillRectangle(d.display, d.root, gc, C.int(r[0]), C.int(r[1]), C.uint(r[2]), C.uint(r[3]))
|
|
78
|
+
C.XSync(d.display, 0)
|
|
79
|
+
changed, err := s.Changed()
|
|
80
|
+
if err != nil || !changed {
|
|
81
|
+
return fmt.Errorf("step %d: missing damage or error: %v", step, err)
|
|
82
|
+
}
|
|
83
|
+
if _, _, err = s.Capture(step == 0); err != nil {
|
|
84
|
+
return err
|
|
85
|
+
}
|
|
86
|
+
if int(d.image.width) < width || int(d.image.height) < height {
|
|
87
|
+
partial++
|
|
88
|
+
}
|
|
89
|
+
if int(d.image.width) != previousWidth || int(d.image.height) != previousHeight {
|
|
90
|
+
resized++
|
|
91
|
+
}
|
|
92
|
+
previousWidth, previousHeight = int(d.image.width), int(d.image.height)
|
|
93
|
+
full := C.XGetImage(d.display, d.root, 0, 0, C.uint(width), C.uint(height), ^C.ulong(0), C.ZPixmap)
|
|
94
|
+
if full == nil {
|
|
95
|
+
return errors.New("cannot capture reference image")
|
|
96
|
+
}
|
|
97
|
+
planes := [3]*C.uint8_t{
|
|
98
|
+
(*C.uint8_t)(unsafe.Pointer(&reference[0])),
|
|
99
|
+
(*C.uint8_t)(unsafe.Pointer(&reference[size])),
|
|
100
|
+
(*C.uint8_t)(unsafe.Pointer(&reference[size*2])),
|
|
101
|
+
}
|
|
102
|
+
var code C.int
|
|
103
|
+
if i420 {
|
|
104
|
+
code = C.ARGBToI420Matrix((*C.uint8_t)(unsafe.Pointer(full.data)), full.bytes_per_line, planes[0], strides[0], planes[1], strides[1], planes[2], strides[2], e.matrix.pointer, C.int(width), C.int(height))
|
|
105
|
+
} else {
|
|
106
|
+
code = C.ARGBToI444Matrix((*C.uint8_t)(unsafe.Pointer(full.data)), full.bytes_per_line, planes[0], strides[0], planes[1], strides[1], planes[2], strides[2], e.matrix.pointer, C.int(width), C.int(height))
|
|
107
|
+
}
|
|
108
|
+
C.XDestroyImage(full)
|
|
109
|
+
if err = avCheck(code); err != nil {
|
|
110
|
+
return err
|
|
111
|
+
}
|
|
112
|
+
for plane := 0; plane < 3; plane++ {
|
|
113
|
+
rows := height
|
|
114
|
+
if plane != 0 {
|
|
115
|
+
rows >>= chroma
|
|
116
|
+
}
|
|
117
|
+
for row := 0; row < rows; row++ {
|
|
118
|
+
expected := reference[size*plane+row*int(strides[plane]) : size*plane+(row+1)*int(strides[plane])]
|
|
119
|
+
actual := unsafe.Slice((*byte)(unsafe.Add(unsafe.Pointer(e.frame.data[plane]), row*int(e.frame.linesize[plane]))), int(strides[plane]))
|
|
120
|
+
if !bytes.Equal(expected, actual) {
|
|
121
|
+
return fmt.Errorf("step %d rect %v: plane %d row %d differs from full conversion", step, r, plane, row)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if partial < 64 || resized < 64 {
|
|
127
|
+
return fmt.Errorf("partial path not exercised: partial=%d resized=%d", partial, resized)
|
|
128
|
+
}
|
|
129
|
+
return nil
|
|
130
|
+
}
|
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
package capture
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
#cgo pkg-config: x11 xext xdamage xfixes libavcodec libavformat libavutil libswscale
|
|
5
|
+
#include "native.h"
|
|
6
|
+
*/
|
|
7
|
+
import "C"
|
|
8
|
+
import (
|
|
9
|
+
"errors"
|
|
10
|
+
"fmt"
|
|
11
|
+
"runtime"
|
|
12
|
+
"strconv"
|
|
13
|
+
"syscall"
|
|
14
|
+
"unsafe"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
func avCheck(code C.int) error {
|
|
18
|
+
if code >= 0 {
|
|
19
|
+
return nil
|
|
20
|
+
}
|
|
21
|
+
var message [128]C.char
|
|
22
|
+
C.av_strerror(code, &message[0], C.size_t(len(message)))
|
|
23
|
+
return errors.New(C.GoString(&message[0]))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type desktop struct {
|
|
27
|
+
display *C.Display
|
|
28
|
+
root C.Window
|
|
29
|
+
attributes C.XWindowAttributes
|
|
30
|
+
damage C.Damage
|
|
31
|
+
damageEvent C.int
|
|
32
|
+
region C.XserverRegion
|
|
33
|
+
image *C.XImage
|
|
34
|
+
shm *C.XShmSegmentInfo
|
|
35
|
+
attached bool
|
|
36
|
+
width, height int
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
func newDesktop(o Options, partial bool) (d *desktop, err error) {
|
|
40
|
+
d = &desktop{width: o.Width, height: o.Height}
|
|
41
|
+
defer func() {
|
|
42
|
+
if err != nil {
|
|
43
|
+
d.close()
|
|
44
|
+
}
|
|
45
|
+
}()
|
|
46
|
+
C.capture_install_handler()
|
|
47
|
+
d.display = C.XOpenDisplay(nil)
|
|
48
|
+
if d.display == nil {
|
|
49
|
+
return d, errors.New("cannot open display")
|
|
50
|
+
}
|
|
51
|
+
d.root = C.XDefaultRootWindow(d.display)
|
|
52
|
+
if C.XGetWindowAttributes(d.display, d.root, &d.attributes) == 0 {
|
|
53
|
+
return d, errors.New("cannot read display dimensions")
|
|
54
|
+
}
|
|
55
|
+
if o.Width > int(d.attributes.width) || o.Height > int(d.attributes.height) {
|
|
56
|
+
return d, errors.New("capture exceeds display dimensions")
|
|
57
|
+
}
|
|
58
|
+
var damageError C.int
|
|
59
|
+
if C.XDamageQueryExtension(d.display, &d.damageEvent, &damageError) == 0 {
|
|
60
|
+
return d, errors.New("XDamage unavailable")
|
|
61
|
+
}
|
|
62
|
+
if C.XShmQueryExtension(d.display) == 0 {
|
|
63
|
+
return d, errors.New("XShm unavailable")
|
|
64
|
+
}
|
|
65
|
+
d.damage = C.XDamageCreate(d.display, d.root, C.XDamageReportNonEmpty)
|
|
66
|
+
if partial {
|
|
67
|
+
d.region = C.XFixesCreateRegion(d.display, nil, 0)
|
|
68
|
+
}
|
|
69
|
+
// XShm retains this address, so the descriptor lives in native memory.
|
|
70
|
+
d.shm = (*C.XShmSegmentInfo)(C.calloc(1, C.sizeof_XShmSegmentInfo))
|
|
71
|
+
if d.shm == nil {
|
|
72
|
+
return d, errors.New("cannot allocate shared memory descriptor")
|
|
73
|
+
}
|
|
74
|
+
d.shm.shmid = -1
|
|
75
|
+
d.image = C.XShmCreateImage(d.display, d.attributes.visual, C.uint(d.attributes.depth), C.ZPixmap, nil, d.shm, C.uint(o.Width), C.uint(o.Height))
|
|
76
|
+
if d.image == nil {
|
|
77
|
+
return d, errors.New("cannot allocate capture image")
|
|
78
|
+
}
|
|
79
|
+
im := d.image
|
|
80
|
+
if im.bits_per_pixel != 32 || im.byte_order != C.LSBFirst || im.red_mask != 0xff0000 || im.green_mask != 0xff00 || im.blue_mask != 0xff {
|
|
81
|
+
return d, errors.New("unsupported screen pixel format")
|
|
82
|
+
}
|
|
83
|
+
d.shm.shmid = C.shmget(C.IPC_PRIVATE, C.size_t(int(im.bytes_per_line)*o.Height), C.IPC_CREAT|0600)
|
|
84
|
+
if d.shm.shmid < 0 {
|
|
85
|
+
return d, errors.New("cannot allocate shared memory")
|
|
86
|
+
}
|
|
87
|
+
address := C.shmat(d.shm.shmid, nil, 0)
|
|
88
|
+
if uintptr(address) == ^uintptr(0) {
|
|
89
|
+
return d, errors.New("cannot map shared memory")
|
|
90
|
+
}
|
|
91
|
+
d.shm.shmaddr = (*C.char)(address)
|
|
92
|
+
im.data = d.shm.shmaddr
|
|
93
|
+
if C.XShmAttach(d.display, d.shm) == 0 {
|
|
94
|
+
return d, errors.New("cannot attach shared memory")
|
|
95
|
+
}
|
|
96
|
+
C.XSync(d.display, 0)
|
|
97
|
+
if C.capture_error() != 0 {
|
|
98
|
+
return d, errors.New("X server rejected shared memory")
|
|
99
|
+
}
|
|
100
|
+
d.attached = true
|
|
101
|
+
// The kernel removes the segment even after a forced process exit.
|
|
102
|
+
if C.shmctl(d.shm.shmid, C.IPC_RMID, nil) != 0 {
|
|
103
|
+
return d, errors.New("cannot mark shared memory for removal")
|
|
104
|
+
}
|
|
105
|
+
return d, nil
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
func (d *desktop) close() {
|
|
109
|
+
if d.attached {
|
|
110
|
+
C.XShmDetach(d.display, d.shm)
|
|
111
|
+
C.XSync(d.display, 0)
|
|
112
|
+
}
|
|
113
|
+
if d.image != nil {
|
|
114
|
+
d.image.data = nil
|
|
115
|
+
C.XDestroyImage(d.image)
|
|
116
|
+
}
|
|
117
|
+
if d.shm != nil {
|
|
118
|
+
if d.shm.shmaddr != nil {
|
|
119
|
+
C.shmdt(unsafe.Pointer(d.shm.shmaddr))
|
|
120
|
+
}
|
|
121
|
+
if d.shm.shmid >= 0 {
|
|
122
|
+
C.shmctl(d.shm.shmid, C.IPC_RMID, nil)
|
|
123
|
+
}
|
|
124
|
+
C.free(unsafe.Pointer(d.shm))
|
|
125
|
+
}
|
|
126
|
+
if d.region != 0 {
|
|
127
|
+
C.XFixesDestroyRegion(d.display, d.region)
|
|
128
|
+
}
|
|
129
|
+
if d.damage != 0 {
|
|
130
|
+
C.XDamageDestroy(d.display, d.damage)
|
|
131
|
+
}
|
|
132
|
+
if d.display != nil {
|
|
133
|
+
C.XCloseDisplay(d.display)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
func (d *desktop) changed() (bool, error) {
|
|
138
|
+
changed := false
|
|
139
|
+
var event C.XEvent
|
|
140
|
+
for C.XPending(d.display) != 0 {
|
|
141
|
+
C.XNextEvent(d.display, &event)
|
|
142
|
+
if C.capture_event_type(&event) == d.damageEvent+C.XDamageNotify {
|
|
143
|
+
changed = true
|
|
144
|
+
if d.region == 0 {
|
|
145
|
+
C.XDamageSubtract(d.display, d.damage, 0, 0)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if C.capture_error() != 0 {
|
|
150
|
+
return false, errors.New("X server capture error")
|
|
151
|
+
}
|
|
152
|
+
return changed, nil
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
func (d *desktop) wait(micros int64) error {
|
|
156
|
+
C.XFlush(d.display)
|
|
157
|
+
fd := C.struct_pollfd{fd: C.XConnectionNumber(d.display), events: C.POLLIN}
|
|
158
|
+
timeout := C.struct_timespec{tv_sec: C.time_t(micros / 1000000), tv_nsec: C.long(micros % 1000000 * 1000)}
|
|
159
|
+
ready, err := C.ppoll(&fd, 1, &timeout, nil)
|
|
160
|
+
if ready < 0 && err != syscall.EINTR {
|
|
161
|
+
return fmt.Errorf("display poll failed: %w", err)
|
|
162
|
+
}
|
|
163
|
+
if fd.revents&(C.POLLERR|C.POLLHUP|C.POLLNVAL) != 0 {
|
|
164
|
+
return errors.New("display disconnected")
|
|
165
|
+
}
|
|
166
|
+
return nil
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
func (d *desktop) capture(first bool) (int, int, error) {
|
|
170
|
+
x, y, right, bottom := 0, 0, d.width, d.height
|
|
171
|
+
if d.region != 0 {
|
|
172
|
+
C.XDamageSubtract(d.display, d.damage, 0, d.region)
|
|
173
|
+
C.XFixesRegionExtents(d.display, d.region, d.region)
|
|
174
|
+
var count C.int
|
|
175
|
+
rectangles := C.XFixesFetchRegion(d.display, d.region, &count)
|
|
176
|
+
if !first && count == 1 && rectangles != nil {
|
|
177
|
+
x = max(0, int(rectangles.x)) &^ 1
|
|
178
|
+
y = max(0, int(rectangles.y)) &^ 1
|
|
179
|
+
right = min(d.width, (int(rectangles.x)+int(rectangles.width)+1)&^1)
|
|
180
|
+
bottom = min(d.height, (int(rectangles.y)+int(rectangles.height)+1)&^1)
|
|
181
|
+
}
|
|
182
|
+
C.XFree(unsafe.Pointer(rectangles))
|
|
183
|
+
if x >= right || y >= bottom {
|
|
184
|
+
x, y, right, bottom = 0, 0, d.width, d.height
|
|
185
|
+
}
|
|
186
|
+
if int(d.image.width) != right-x || int(d.image.height) != bottom-y {
|
|
187
|
+
next := C.XShmCreateImage(d.display, d.attributes.visual, C.uint(d.attributes.depth), C.ZPixmap, d.shm.shmaddr, d.shm, C.uint(right-x), C.uint(bottom-y))
|
|
188
|
+
if next == nil {
|
|
189
|
+
return 0, 0, errors.New("cannot allocate capture region")
|
|
190
|
+
}
|
|
191
|
+
d.image.data = nil
|
|
192
|
+
C.XDestroyImage(d.image)
|
|
193
|
+
d.image = next
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if C.XShmGetImage(d.display, d.root, d.image, C.int(x), C.int(y), ^C.ulong(0)) == 0 {
|
|
197
|
+
return 0, 0, errors.New("screen capture failed")
|
|
198
|
+
}
|
|
199
|
+
return x, y, nil
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
type dictionary struct{ pointer *C.AVDictionary }
|
|
203
|
+
|
|
204
|
+
func (d *dictionary) set(key, value string) error {
|
|
205
|
+
k, v := C.CString(key), C.CString(value)
|
|
206
|
+
defer C.free(unsafe.Pointer(k))
|
|
207
|
+
defer C.free(unsafe.Pointer(v))
|
|
208
|
+
return avCheck(C.av_dict_set(&d.pointer, k, v, 0))
|
|
209
|
+
}
|
|
210
|
+
func (d *dictionary) close() { C.av_dict_free(&d.pointer) }
|
|
211
|
+
|
|
212
|
+
type encoder struct {
|
|
213
|
+
codec *C.AVCodecContext
|
|
214
|
+
mux *C.AVFormatContext
|
|
215
|
+
stream *C.AVStream
|
|
216
|
+
frame, input *C.AVFrame
|
|
217
|
+
packet *C.AVPacket
|
|
218
|
+
scale *C.struct_SwsContext
|
|
219
|
+
matrix colorMatrix
|
|
220
|
+
headerWritten bool
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
func newEncoder(o Options) (e *encoder, err error) {
|
|
224
|
+
e = &encoder{}
|
|
225
|
+
defer func() {
|
|
226
|
+
if err != nil {
|
|
227
|
+
e.close()
|
|
228
|
+
}
|
|
229
|
+
}()
|
|
230
|
+
if hasLibyuv {
|
|
231
|
+
e.matrix, err = newColorMatrix(o.BT709 != 0)
|
|
232
|
+
if err != nil {
|
|
233
|
+
return e, err
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
name := C.CString("libx264")
|
|
237
|
+
defer C.free(unsafe.Pointer(name))
|
|
238
|
+
implementation := C.avcodec_find_encoder_by_name(name)
|
|
239
|
+
if implementation == nil {
|
|
240
|
+
return e, errors.New("libx264 unavailable")
|
|
241
|
+
}
|
|
242
|
+
e.codec = C.avcodec_alloc_context3(implementation)
|
|
243
|
+
if e.codec == nil {
|
|
244
|
+
return e, errors.New("cannot allocate encoder")
|
|
245
|
+
}
|
|
246
|
+
c := e.codec
|
|
247
|
+
c.width, c.height = C.int(o.Width), C.int(o.Height)
|
|
248
|
+
c.pix_fmt = C.AV_PIX_FMT_YUV420P
|
|
249
|
+
if o.Profile == "high444" {
|
|
250
|
+
c.pix_fmt = C.AV_PIX_FMT_YUV444P
|
|
251
|
+
}
|
|
252
|
+
c.time_base = C.AVRational{num: 1, den: 90000}
|
|
253
|
+
c.framerate = C.AVRational{num: C.int(o.FPS), den: 1}
|
|
254
|
+
c.gop_size = C.int(max(1, (o.FPS*o.KeyframeMS+500)/1000))
|
|
255
|
+
c.max_b_frames, c.thread_count = 0, C.int(o.Threads)
|
|
256
|
+
c.rc_max_rate = C.int64_t(o.Kbps) * 1000
|
|
257
|
+
c.rc_buffer_size = C.int(o.Kbps * min(o.KeyframeMS, 100))
|
|
258
|
+
c.flags |= C.AV_CODEC_FLAG_LOW_DELAY
|
|
259
|
+
if o.BT709 != 0 {
|
|
260
|
+
c.color_primaries = C.AVCOL_PRI_BT709
|
|
261
|
+
c.color_trc = C.AVCOL_TRC_BT709
|
|
262
|
+
c.colorspace = C.AVCOL_SPC_BT709
|
|
263
|
+
c.color_range = C.AVCOL_RANGE_MPEG
|
|
264
|
+
}
|
|
265
|
+
dict := dictionary{}
|
|
266
|
+
defer dict.close()
|
|
267
|
+
parameters := fmt.Sprintf("fps=%d/1:repeat-headers=1:sliced-threads=1:slices=4:keyint=%d:min-keyint=%d:scenecut=0:chroma-qp-offset=%d:intra-refresh=%d", o.FPS, c.gop_size, c.gop_size, o.ChromaQP, o.IntraRefresh)
|
|
268
|
+
if o.Preset == "superfast" {
|
|
269
|
+
parameters += ":subme=0"
|
|
270
|
+
}
|
|
271
|
+
for _, option := range [][2]string{{"preset", o.Preset}, {"tune", "zerolatency"}, {"crf", strconv.Itoa(o.CRF)}, {"profile", o.Profile}, {"level", "4.2"}, {"forced-idr", "1"}, {"x264-params", parameters}} {
|
|
272
|
+
if err = dict.set(option[0], option[1]); err != nil {
|
|
273
|
+
return e, err
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if err = avCheck(C.avcodec_open2(c, implementation, &dict.pointer)); err != nil {
|
|
277
|
+
return e, err
|
|
278
|
+
}
|
|
279
|
+
dict.close()
|
|
280
|
+
format, url := C.CString("rtp"), C.CString(o.URL)
|
|
281
|
+
defer C.free(unsafe.Pointer(format))
|
|
282
|
+
defer C.free(unsafe.Pointer(url))
|
|
283
|
+
if err = avCheck(C.avformat_alloc_output_context2(&e.mux, nil, format, url)); err != nil {
|
|
284
|
+
return e, err
|
|
285
|
+
}
|
|
286
|
+
e.stream = C.avformat_new_stream(e.mux, nil)
|
|
287
|
+
if e.stream == nil {
|
|
288
|
+
return e, errors.New("cannot allocate RTP stream")
|
|
289
|
+
}
|
|
290
|
+
e.stream.time_base = c.time_base
|
|
291
|
+
if err = avCheck(C.avcodec_parameters_from_context(e.stream.codecpar, c)); err != nil {
|
|
292
|
+
return e, err
|
|
293
|
+
}
|
|
294
|
+
if err = dict.set("payload_type", strconv.Itoa(o.Payload)); err != nil {
|
|
295
|
+
return e, err
|
|
296
|
+
}
|
|
297
|
+
if err = dict.set("ssrc", o.SSRC); err != nil {
|
|
298
|
+
return e, err
|
|
299
|
+
}
|
|
300
|
+
e.mux.flags |= C.AVFMT_FLAG_FLUSH_PACKETS
|
|
301
|
+
if err = avCheck(C.avio_open(&e.mux.pb, url, C.AVIO_FLAG_WRITE)); err != nil {
|
|
302
|
+
return e, err
|
|
303
|
+
}
|
|
304
|
+
if err = avCheck(C.avformat_write_header(e.mux, &dict.pointer)); err != nil {
|
|
305
|
+
return e, err
|
|
306
|
+
}
|
|
307
|
+
e.headerWritten = true
|
|
308
|
+
if !hasLibyuv {
|
|
309
|
+
e.scale = C.sws_alloc_context()
|
|
310
|
+
if e.scale == nil {
|
|
311
|
+
return e, errors.New("cannot allocate scaler")
|
|
312
|
+
}
|
|
313
|
+
for _, option := range []struct {
|
|
314
|
+
key string
|
|
315
|
+
value int
|
|
316
|
+
}{{"srcw", o.Width}, {"srch", o.Height}, {"src_format", C.AV_PIX_FMT_BGR0}, {"dstw", o.Width}, {"dsth", o.Height}, {"dst_format", int(c.pix_fmt)}, {"sws_flags", C.SWS_LANCZOS}, {"threads", o.Threads}} {
|
|
317
|
+
key := C.CString(option.key)
|
|
318
|
+
err = avCheck(C.av_opt_set_int(unsafe.Pointer(e.scale), key, C.int64_t(option.value), 0))
|
|
319
|
+
C.free(unsafe.Pointer(key))
|
|
320
|
+
if err != nil {
|
|
321
|
+
return e, err
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if err = avCheck(C.sws_init_context(e.scale, nil, nil)); err != nil {
|
|
325
|
+
return e, err
|
|
326
|
+
}
|
|
327
|
+
if o.BT709 != 0 {
|
|
328
|
+
coefficients := C.sws_getCoefficients(C.SWS_CS_ITU709)
|
|
329
|
+
if err = avCheck(C.sws_setColorspaceDetails(e.scale, coefficients, 0, coefficients, 0, 0, 1<<16, 1<<16)); err != nil {
|
|
330
|
+
return e, err
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
e.input = C.av_frame_alloc()
|
|
334
|
+
if e.input == nil {
|
|
335
|
+
return e, errors.New("cannot allocate input frame")
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
e.frame, e.packet = C.av_frame_alloc(), C.av_packet_alloc()
|
|
339
|
+
if e.frame == nil || e.packet == nil {
|
|
340
|
+
return e, errors.New("cannot allocate frame or packet")
|
|
341
|
+
}
|
|
342
|
+
e.frame.format, e.frame.width, e.frame.height = C.int(c.pix_fmt), c.width, c.height
|
|
343
|
+
e.frame.color_primaries, e.frame.color_trc = c.color_primaries, c.color_trc
|
|
344
|
+
e.frame.colorspace, e.frame.color_range = c.colorspace, c.color_range
|
|
345
|
+
if err = avCheck(C.av_frame_get_buffer(e.frame, 32)); err != nil {
|
|
346
|
+
return e, err
|
|
347
|
+
}
|
|
348
|
+
return e, nil
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
func (e *encoder) close() {
|
|
352
|
+
if e.headerWritten {
|
|
353
|
+
C.av_write_trailer(e.mux)
|
|
354
|
+
}
|
|
355
|
+
C.av_packet_free(&e.packet)
|
|
356
|
+
C.av_frame_free(&e.frame)
|
|
357
|
+
C.av_frame_free(&e.input)
|
|
358
|
+
C.avcodec_free_context(&e.codec)
|
|
359
|
+
if e.mux != nil {
|
|
360
|
+
if e.mux.pb != nil {
|
|
361
|
+
C.avio_closep(&e.mux.pb)
|
|
362
|
+
}
|
|
363
|
+
C.avformat_free_context(e.mux)
|
|
364
|
+
}
|
|
365
|
+
C.sws_freeContext(e.scale)
|
|
366
|
+
e.matrix.close()
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
func (e *encoder) convert(d *desktop, x, y int) error {
|
|
370
|
+
im := d.image
|
|
371
|
+
if e.scale != nil {
|
|
372
|
+
e.input.format, e.input.width, e.input.height = C.AV_PIX_FMT_BGR0, C.int(d.width), C.int(d.height)
|
|
373
|
+
e.input.buf[0] = C.capture_reference((*C.uint8_t)(unsafe.Pointer(im.data)), C.size_t(int(im.bytes_per_line)*d.height))
|
|
374
|
+
if e.input.buf[0] == nil {
|
|
375
|
+
return errors.New("cannot reference shared memory")
|
|
376
|
+
}
|
|
377
|
+
e.input.data[0], e.input.linesize[0] = (*C.uint8_t)(unsafe.Pointer(im.data)), im.bytes_per_line
|
|
378
|
+
err := avCheck(C.sws_scale_frame(e.scale, e.frame, e.input))
|
|
379
|
+
C.av_frame_unref(e.input)
|
|
380
|
+
return err
|
|
381
|
+
}
|
|
382
|
+
return e.matrix.convert(im, e.frame, x, y)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
func (e *encoder) send(pts int64, keyframe bool) error {
|
|
386
|
+
e.frame.pts, e.frame.pict_type = C.int64_t(pts), C.AV_PICTURE_TYPE_NONE
|
|
387
|
+
if keyframe {
|
|
388
|
+
e.frame.pict_type = C.AV_PICTURE_TYPE_I
|
|
389
|
+
}
|
|
390
|
+
if err := avCheck(C.avcodec_send_frame(e.codec, e.frame)); err != nil {
|
|
391
|
+
return err
|
|
392
|
+
}
|
|
393
|
+
for {
|
|
394
|
+
code := C.avcodec_receive_packet(e.codec, e.packet)
|
|
395
|
+
if code == -C.EAGAIN || code == C.CAPTURE_AVERROR_EOF {
|
|
396
|
+
return nil
|
|
397
|
+
}
|
|
398
|
+
if err := avCheck(code); err != nil {
|
|
399
|
+
return err
|
|
400
|
+
}
|
|
401
|
+
C.av_packet_rescale_ts(e.packet, e.codec.time_base, e.stream.time_base)
|
|
402
|
+
e.packet.stream_index = e.stream.index
|
|
403
|
+
err := avCheck(C.av_interleaved_write_frame(e.mux, e.packet))
|
|
404
|
+
C.av_packet_unref(e.packet)
|
|
405
|
+
if err != nil {
|
|
406
|
+
return err
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Open and every method on the stream must run on the same goroutine.
|
|
412
|
+
// Xlib state is confined to its locked OS thread until Close.
|
|
413
|
+
type Stream struct {
|
|
414
|
+
desktop *desktop
|
|
415
|
+
encoder *encoder
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
func Open(o Options) (*Stream, error) {
|
|
419
|
+
runtime.LockOSThread()
|
|
420
|
+
C.av_log_set_level(C.AV_LOG_WARNING)
|
|
421
|
+
d, err := newDesktop(o, hasLibyuv)
|
|
422
|
+
if err != nil {
|
|
423
|
+
runtime.UnlockOSThread()
|
|
424
|
+
return nil, err
|
|
425
|
+
}
|
|
426
|
+
e, err := newEncoder(o)
|
|
427
|
+
if err != nil {
|
|
428
|
+
d.close()
|
|
429
|
+
runtime.UnlockOSThread()
|
|
430
|
+
return nil, err
|
|
431
|
+
}
|
|
432
|
+
return &Stream{desktop: d, encoder: e}, nil
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
func (s *Stream) Close() {
|
|
436
|
+
if s.desktop == nil {
|
|
437
|
+
return
|
|
438
|
+
}
|
|
439
|
+
s.encoder.close()
|
|
440
|
+
s.desktop.close()
|
|
441
|
+
s.encoder, s.desktop = nil, nil
|
|
442
|
+
runtime.UnlockOSThread()
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
func NowMicros() int64 { return int64(C.av_gettime_relative()) }
|
|
446
|
+
func (s *Stream) Changed() (bool, error) { return s.desktop.changed() }
|
|
447
|
+
func (s *Stream) Wait(micros int64) error { return s.desktop.wait(micros) }
|
|
448
|
+
func (s *Stream) Send(pts int64, keyframe bool) error { return s.encoder.send(pts, keyframe) }
|
|
449
|
+
|
|
450
|
+
func (s *Stream) Capture(first bool) (copied, converted int64, err error) {
|
|
451
|
+
if err = avCheck(C.av_frame_make_writable(s.encoder.frame)); err != nil {
|
|
452
|
+
return
|
|
453
|
+
}
|
|
454
|
+
x, y, err := s.desktop.capture(first)
|
|
455
|
+
if err != nil {
|
|
456
|
+
return
|
|
457
|
+
}
|
|
458
|
+
copied = NowMicros()
|
|
459
|
+
err = s.encoder.convert(s.desktop, x, y)
|
|
460
|
+
converted = NowMicros()
|
|
461
|
+
return
|
|
462
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#define _GNU_SOURCE
|
|
2
|
+
#define XUTIL_DEFINE_FUNCTIONS
|
|
3
|
+
#include <X11/Xlib.h>
|
|
4
|
+
#include <X11/Xutil.h>
|
|
5
|
+
#include <X11/extensions/XShm.h>
|
|
6
|
+
#include <X11/extensions/Xdamage.h>
|
|
7
|
+
#include <X11/extensions/Xfixes.h>
|
|
8
|
+
#include <errno.h>
|
|
9
|
+
#include <poll.h>
|
|
10
|
+
#include <stdlib.h>
|
|
11
|
+
#include <sys/shm.h>
|
|
12
|
+
#include <libavcodec/avcodec.h>
|
|
13
|
+
#include <libavformat/avformat.h>
|
|
14
|
+
#include <libavutil/opt.h>
|
|
15
|
+
#include <libavutil/time.h>
|
|
16
|
+
#include <libswscale/swscale.h>
|
|
17
|
+
|
|
18
|
+
static int capture_x_error;
|
|
19
|
+
static int capture_handle_error(Display *display, XErrorEvent *event) {
|
|
20
|
+
capture_x_error = event->error_code;
|
|
21
|
+
return 0;
|
|
22
|
+
}
|
|
23
|
+
static void capture_install_handler(void) { capture_x_error = 0; XSetErrorHandler(capture_handle_error); }
|
|
24
|
+
static int capture_error(void) { return capture_x_error; }
|
|
25
|
+
static int capture_event_type(XEvent *event) { return event->type; }
|
|
26
|
+
// XShm owns the pixels; swscale only borrows them synchronously.
|
|
27
|
+
static void capture_keep_shm(void *opaque, uint8_t *data) {}
|
|
28
|
+
static AVBufferRef *capture_reference(uint8_t *data, size_t size) {
|
|
29
|
+
return av_buffer_create(data, size, capture_keep_shm, NULL, AV_BUFFER_FLAG_READONLY);
|
|
30
|
+
}
|
|
31
|
+
enum { CAPTURE_AVERROR_EOF = AVERROR_EOF };
|