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.
@@ -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
- stats.videoFps = (counters.framesDecoded - inbound.framesDecoded) / seconds;
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,3 @@
1
+ module github.com/replicas-group/replicas/computer-use/native
2
+
3
+ go 1.27.1
@@ -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 };