fix jpeg
This commit is contained in:
@@ -1648,9 +1648,9 @@ func encodeBlocksbackstage(img *image.RGBA, prev *prevImage, quality int, codec
|
|||||||
}
|
}
|
||||||
|
|
||||||
prevCopyStart := time.Now()
|
prevCopyStart := time.Now()
|
||||||
prevMu.Lock()
|
backstagePrevMu.Lock()
|
||||||
copyPrev(img)
|
copyPrevbackstage(img)
|
||||||
prevMu.Unlock()
|
backstagePrevMu.Unlock()
|
||||||
statPrevCopyNs.Add(time.Since(prevCopyStart).Nanoseconds())
|
statPrevCopyNs.Add(time.Since(prevCopyStart).Nanoseconds())
|
||||||
|
|
||||||
out := make([]byte, buf.Len())
|
out := make([]byte, buf.Len())
|
||||||
|
|||||||
@@ -23,6 +23,51 @@ type recordingWriter struct {
|
|||||||
types []websocket.MessageType
|
types []websocket.MessageType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEncodeBlocksBackstageAdvancesOnlyBackstageHistory(t *testing.T) {
|
||||||
|
img := image.NewRGBA(image.Rect(0, 0, 64, 64))
|
||||||
|
for i := range img.Pix {
|
||||||
|
img.Pix[i] = 0xff
|
||||||
|
}
|
||||||
|
previous := &prevImage{w: 64, h: 64, pix: make([]byte, len(img.Pix))}
|
||||||
|
desktopMarker := &prevImage{w: 1, h: 1, pix: []byte{0x42}}
|
||||||
|
|
||||||
|
prevMu.Lock()
|
||||||
|
savedDesktop := prevFrame
|
||||||
|
prevFrame = desktopMarker
|
||||||
|
prevMu.Unlock()
|
||||||
|
backstagePrevMu.Lock()
|
||||||
|
savedBackstage := backstagePrevFrame
|
||||||
|
backstagePrevFrame = nil
|
||||||
|
backstagePrevMu.Unlock()
|
||||||
|
t.Cleanup(func() {
|
||||||
|
prevMu.Lock()
|
||||||
|
prevFrame = savedDesktop
|
||||||
|
prevMu.Unlock()
|
||||||
|
backstagePrevMu.Lock()
|
||||||
|
backstagePrevFrame = savedBackstage
|
||||||
|
backstagePrevMu.Unlock()
|
||||||
|
})
|
||||||
|
|
||||||
|
regions, _, _, err := encodeBlocksbackstage(img, previous, 90, "raw", 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("encodeBlocksbackstage: %v", err)
|
||||||
|
}
|
||||||
|
if regions == 0 {
|
||||||
|
t.Fatal("expected changed backstage regions")
|
||||||
|
}
|
||||||
|
|
||||||
|
prevMu.Lock()
|
||||||
|
if prevFrame != desktopMarker || !bytes.Equal(prevFrame.pix, []byte{0x42}) {
|
||||||
|
t.Error("backstage block encode modified desktop history")
|
||||||
|
}
|
||||||
|
prevMu.Unlock()
|
||||||
|
backstagePrevMu.Lock()
|
||||||
|
if backstagePrevFrame == nil || !bytes.Equal(backstagePrevFrame.pix, img.Pix) {
|
||||||
|
t.Error("backstage block encode did not advance backstage history")
|
||||||
|
}
|
||||||
|
backstagePrevMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
func (w *recordingWriter) Write(ctx context.Context, messageType websocket.MessageType, p []byte) error {
|
func (w *recordingWriter) Write(ctx context.Context, messageType websocket.MessageType, p []byte) error {
|
||||||
w.types = append(w.types, messageType)
|
w.types = append(w.types, messageType)
|
||||||
w.msgs = append(w.msgs, append([]byte(nil), p...))
|
w.msgs = append(w.msgs, append([]byte(nil), p...))
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package capture
|
||||||
|
|
||||||
|
func jpegEncoderQuality(quality int) int {
|
||||||
|
if quality < 1 {
|
||||||
|
quality = 1
|
||||||
|
}
|
||||||
|
if quality > 100 {
|
||||||
|
quality = 100
|
||||||
|
}
|
||||||
|
deficit := 100 - quality
|
||||||
|
return 100 - (deficit*deficit+99)/100
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//go:build !turbojpeg
|
||||||
|
|
||||||
package capture
|
package capture
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -17,7 +19,7 @@ var jpegBufPool = sync.Pool{
|
|||||||
func encodeJPEG(img image.Image, quality int) ([]byte, error) {
|
func encodeJPEG(img image.Image, quality int) ([]byte, error) {
|
||||||
buf := jpegBufPool.Get().(*bytes.Buffer)
|
buf := jpegBufPool.Get().(*bytes.Buffer)
|
||||||
buf.Reset()
|
buf.Reset()
|
||||||
err := jpeg.Encode(buf, img, &jpeg.Options{Quality: quality})
|
err := jpeg.Encode(buf, img, &jpeg.Options{Quality: jpegEncoderQuality(quality)})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
jpegBufPool.Put(buf)
|
jpegBufPool.Put(buf)
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -31,7 +33,7 @@ func encodeJPEG(img image.Image, quality int) ([]byte, error) {
|
|||||||
func encodeJPEGToBuf(dst *bytes.Buffer, img image.Image, quality int) error {
|
func encodeJPEGToBuf(dst *bytes.Buffer, img image.Image, quality int) error {
|
||||||
buf := jpegBufPool.Get().(*bytes.Buffer)
|
buf := jpegBufPool.Get().(*bytes.Buffer)
|
||||||
buf.Reset()
|
buf.Reset()
|
||||||
if err := jpeg.Encode(buf, img, &jpeg.Options{Quality: quality}); err != nil {
|
if err := jpeg.Encode(buf, img, &jpeg.Options{Quality: jpegEncoderQuality(quality)}); err != nil {
|
||||||
jpegBufPool.Put(buf)
|
jpegBufPool.Put(buf)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package capture
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestJPEGEncoderQualityCurve(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
input int
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{input: -1, want: 1},
|
||||||
|
{input: 20, want: 36},
|
||||||
|
{input: 50, want: 75},
|
||||||
|
{input: 80, want: 96},
|
||||||
|
{input: 90, want: 99},
|
||||||
|
{input: 100, want: 100},
|
||||||
|
{input: 101, want: 100},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
if got := jpegEncoderQuality(tt.input); got != tt.want {
|
||||||
|
t.Errorf("jpegEncoderQuality(%d) = %d, want %d", tt.input, got, tt.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,18 +5,19 @@ package capture
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"image"
|
"image"
|
||||||
|
"image/color"
|
||||||
|
|
||||||
ljpeg "github.com/pixiv/go-libjpeg/jpeg"
|
ljpeg "github.com/pixiv/go-libjpeg/jpeg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func encodeJPEG(img image.Image, quality int) ([]byte, error) {
|
func encodeJPEG(img image.Image, quality int) ([]byte, error) {
|
||||||
opts := &ljpeg.EncoderOptions{
|
opts := &ljpeg.EncoderOptions{
|
||||||
Quality: quality,
|
Quality: jpegEncoderQuality(quality),
|
||||||
OptimizeCoding: true,
|
OptimizeCoding: true,
|
||||||
DCTMethod: ljpeg.DCTISlow,
|
DCTMethod: ljpeg.DCTISlow,
|
||||||
}
|
}
|
||||||
buf := bytes.Buffer{}
|
buf := bytes.Buffer{}
|
||||||
if err := ljpeg.Encode(&buf, img, opts); err != nil {
|
if err := ljpeg.Encode(&buf, jpeg444Image(img), opts); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return buf.Bytes(), nil
|
return buf.Bytes(), nil
|
||||||
@@ -24,9 +25,49 @@ func encodeJPEG(img image.Image, quality int) ([]byte, error) {
|
|||||||
|
|
||||||
func encodeJPEGToBuf(dst *bytes.Buffer, img image.Image, quality int) error {
|
func encodeJPEGToBuf(dst *bytes.Buffer, img image.Image, quality int) error {
|
||||||
opts := &ljpeg.EncoderOptions{
|
opts := &ljpeg.EncoderOptions{
|
||||||
Quality: quality,
|
Quality: jpegEncoderQuality(quality),
|
||||||
OptimizeCoding: true,
|
OptimizeCoding: true,
|
||||||
DCTMethod: ljpeg.DCTISlow,
|
DCTMethod: ljpeg.DCTISlow,
|
||||||
}
|
}
|
||||||
return ljpeg.Encode(dst, img, opts)
|
return ljpeg.Encode(dst, jpeg444Image(img), opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// libjpeg preserves the source YCbCr sampling factors, unlike Go's standard
|
||||||
|
// JPEG encoder, so TurboJPEG builds can retain full chroma resolution.
|
||||||
|
func jpeg444Image(src image.Image) image.Image {
|
||||||
|
if src == nil {
|
||||||
|
return src
|
||||||
|
}
|
||||||
|
if ycbcr, ok := src.(*image.YCbCr); ok && ycbcr.SubsampleRatio == image.YCbCrSubsampleRatio444 {
|
||||||
|
return src
|
||||||
|
}
|
||||||
|
|
||||||
|
bounds := src.Bounds()
|
||||||
|
dst := image.NewYCbCr(bounds, image.YCbCrSubsampleRatio444)
|
||||||
|
if rgba, ok := src.(*image.RGBA); ok {
|
||||||
|
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||||
|
srcOffset := rgba.PixOffset(bounds.Min.X, y)
|
||||||
|
dstOffset := dst.YOffset(bounds.Min.X, y)
|
||||||
|
for x := 0; x < bounds.Dx(); x++ {
|
||||||
|
i := srcOffset + x*4
|
||||||
|
yy, cb, cr := color.RGBToYCbCr(rgba.Pix[i], rgba.Pix[i+1], rgba.Pix[i+2])
|
||||||
|
dst.Y[dstOffset+x] = yy
|
||||||
|
dst.Cb[dstOffset+x] = cb
|
||||||
|
dst.Cr[dstOffset+x] = cr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dst
|
||||||
|
}
|
||||||
|
|
||||||
|
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||||
|
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||||
|
pixel := color.NRGBAModel.Convert(src.At(x, y)).(color.NRGBA)
|
||||||
|
yy, cb, cr := color.RGBToYCbCr(pixel.R, pixel.G, pixel.B)
|
||||||
|
offset := dst.YOffset(x, y)
|
||||||
|
dst.Y[offset] = yy
|
||||||
|
dst.Cb[offset] = cb
|
||||||
|
dst.Cr[offset] = cr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dst
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,7 +149,10 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
|||||||
let moveFrame = 0;
|
let moveFrame = 0;
|
||||||
let videoDecoder = null;
|
let videoDecoder = null;
|
||||||
let h264TimestampUs = 0;
|
let h264TimestampUs = 0;
|
||||||
|
let h264AwaitingKeyframe = true;
|
||||||
|
let h264RecoveryRequestedAt = 0;
|
||||||
let prefersH264 = typeof VideoDecoder === "function";
|
let prefersH264 = typeof VideoDecoder === "function";
|
||||||
|
const maxH264DecodeQueue = 4;
|
||||||
const inputBackpressureBytes = 256 * 1024;
|
const inputBackpressureBytes = 256 * 1024;
|
||||||
let h264ErrorCount = 0;
|
let h264ErrorCount = 0;
|
||||||
let h264RetryTimer = null;
|
let h264RetryTimer = null;
|
||||||
@@ -1114,6 +1117,15 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
|||||||
}
|
}
|
||||||
videoDecoder = null;
|
videoDecoder = null;
|
||||||
h264TimestampUs = 0;
|
h264TimestampUs = 0;
|
||||||
|
h264AwaitingKeyframe = true;
|
||||||
|
h264RecoveryRequestedAt = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestH264DecoderRecovery(reason) {
|
||||||
|
const now = performance.now();
|
||||||
|
if (now - h264RecoveryRequestedAt < 500) return;
|
||||||
|
h264RecoveryRequestedAt = now;
|
||||||
|
sendCmd("backstage_request_keyframe", { reason });
|
||||||
}
|
}
|
||||||
|
|
||||||
function fallbackToJpegCodec(reason) {
|
function fallbackToJpegCodec(reason) {
|
||||||
@@ -1189,6 +1201,8 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
|||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
console.warn("backstage: h264 decoder error", err);
|
console.warn("backstage: h264 decoder error", err);
|
||||||
|
h264AwaitingKeyframe = true;
|
||||||
|
requestH264DecoderRecovery("h264_decoder_error");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
videoDecoder.configure({ codec: "avc1.42E01E", optimizeForLatency: true });
|
videoDecoder.configure({ codec: "avc1.42E01E", optimizeForLatency: true });
|
||||||
@@ -1416,9 +1430,23 @@ import { createSharedUiSettingsSaver, loadSharedUiSettings } from "./shared-ui-s
|
|||||||
fallbackToJpegCodec("WebCodecs decoder unavailable");
|
fallbackToJpegCodec("WebCodecs decoder unavailable");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const isKeyFrame = isH264KeyFrame(h264Bytes);
|
||||||
|
if (videoDecoder.decodeQueueSize > maxH264DecodeQueue) {
|
||||||
|
videoDecoder.reset();
|
||||||
|
h264TimestampUs = 0;
|
||||||
|
h264AwaitingKeyframe = true;
|
||||||
|
requestH264DecoderRecovery("h264_decoder_backlog");
|
||||||
|
}
|
||||||
|
if (h264AwaitingKeyframe && !isKeyFrame) {
|
||||||
|
requestH264DecoderRecovery("h264_decoder_keyframe_required");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isKeyFrame) {
|
||||||
|
h264AwaitingKeyframe = false;
|
||||||
|
}
|
||||||
const frameIntervalUs = Math.floor(1_000_000 / Math.max(1, fps || 25));
|
const frameIntervalUs = Math.floor(1_000_000 / Math.max(1, fps || 25));
|
||||||
const chunk = new EncodedVideoChunk({
|
const chunk = new EncodedVideoChunk({
|
||||||
type: isH264KeyFrame(h264Bytes) ? "key" : "delta",
|
type: isKeyFrame ? "key" : "delta",
|
||||||
timestamp: h264TimestampUs,
|
timestamp: h264TimestampUs,
|
||||||
data: h264Bytes,
|
data: h264Bytes,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user