start of fix
This commit is contained in:
@@ -110,10 +110,9 @@ func CaptureAndSend(ctx context.Context, env *rt.Env) error {
|
|||||||
|
|
||||||
if frame, captureDur, encodeDur, used, err := tryBuildDirectH264Frame(display); used {
|
if frame, captureDur, encodeDur, used, err := tryBuildDirectH264Frame(display); used {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
consecutiveCaptureFails.Add(1)
|
suppressDesktopHardwareH264(err)
|
||||||
log.Printf("capture: direct video capture failed: %v (sending black frame, consecutive=%d)", err, consecutiveCaptureFails.Load())
|
log.Printf("capture: direct video encode failed: %v; falling back to the software H.264 path", err)
|
||||||
return sendBlackFrame(ctx, env)
|
} else {
|
||||||
}
|
|
||||||
emitDesktopCursor(ctx, env, display, frame.Header.Width, frame.Header.Height)
|
emitDesktopCursor(ctx, env, display, frame.Header.Width, frame.Header.Height)
|
||||||
if len(frame.Data) == 0 {
|
if len(frame.Data) == 0 {
|
||||||
consecutiveCaptureFails.Store(0)
|
consecutiveCaptureFails.Store(0)
|
||||||
@@ -122,6 +121,7 @@ func CaptureAndSend(ctx context.Context, env *rt.Env) error {
|
|||||||
consecutiveCaptureFails.Store(0)
|
consecutiveCaptureFails.Store(0)
|
||||||
return sendCompletedFrame(ctx, env, frame, display, t0, captureDur, encodeDur)
|
return sendCompletedFrame(ctx, env, frame, display, t0, captureDur, encodeDur)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
img, err := safeCaptureDisplay(display)
|
img, err := safeCaptureDisplay(display)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -145,7 +145,7 @@ func CaptureAndSend(ctx context.Context, env *rt.Env) error {
|
|||||||
consecutiveCaptureFails.Store(0)
|
consecutiveCaptureFails.Store(0)
|
||||||
captureDur := time.Since(t0)
|
captureDur := time.Since(t0)
|
||||||
|
|
||||||
willSendViaWebRTC := blockCodec() == "h264" && webrtcpub.IsActive(webrtcpub.KindDesktop)
|
willSendViaWebRTC := desktopCodec() == "h264" && webrtcpub.IsActive(webrtcpub.KindDesktop)
|
||||||
var slotAcquired bool
|
var slotAcquired bool
|
||||||
if !willSendViaWebRTC {
|
if !willSendViaWebRTC {
|
||||||
if !AcquireFrameSlot() {
|
if !AcquireFrameSlot() {
|
||||||
@@ -157,7 +157,7 @@ func CaptureAndSend(ctx context.Context, env *rt.Env) error {
|
|||||||
|
|
||||||
frameWidth := img.Bounds().Dx()
|
frameWidth := img.Bounds().Dx()
|
||||||
frameHeight := img.Bounds().Dy()
|
frameHeight := img.Bounds().Dy()
|
||||||
quality := jpegQuality()
|
quality := desktopJPEGQuality()
|
||||||
frame, encodeDur, err := buildFrame(img, display, quality)
|
frame, encodeDur, err := buildFrame(img, display, quality)
|
||||||
PutRGBA(img)
|
PutRGBA(img)
|
||||||
img = nil
|
img = nil
|
||||||
@@ -413,7 +413,7 @@ func captureAllDisplaysAndSend(ctx context.Context, env *rt.Env) error {
|
|||||||
PutRGBA(part.img)
|
PutRGBA(part.img)
|
||||||
}
|
}
|
||||||
|
|
||||||
quality := jpegQuality()
|
quality := desktopJPEGQuality()
|
||||||
frame, _, err := buildFrame(canvas, 0, quality)
|
frame, _, err := buildFrame(canvas, 0, quality)
|
||||||
canvasW, canvasH := canvas.Rect.Dx(), canvas.Rect.Dy()
|
canvasW, canvasH := canvas.Rect.Dx(), canvas.Rect.Dy()
|
||||||
PutRGBA(canvas)
|
PutRGBA(canvas)
|
||||||
@@ -482,6 +482,8 @@ func MonitorCount() int {
|
|||||||
|
|
||||||
const frameLogInterval = 5 * time.Second
|
const frameLogInterval = 5 * time.Second
|
||||||
|
|
||||||
|
const desktopHardwareH264FallbackCooldown = 30 * time.Second
|
||||||
|
|
||||||
var consecutiveCaptureFails atomic.Int64
|
var consecutiveCaptureFails atomic.Int64
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -511,10 +513,14 @@ var (
|
|||||||
statBlockFallbacks atomic.Int64
|
statBlockFallbacks atomic.Int64
|
||||||
statFrameSlotSkips atomic.Int64
|
statFrameSlotSkips atomic.Int64
|
||||||
|
|
||||||
overrideQuality atomic.Int64
|
desktopOverrideQuality atomic.Int64
|
||||||
overrideCodec atomic.Value
|
backstageOverrideQuality atomic.Int64
|
||||||
|
desktopOverrideCodec atomic.Value
|
||||||
|
backstageOverrideCodec atomic.Value
|
||||||
desktopSoftwareH264 atomic.Bool
|
desktopSoftwareH264 atomic.Bool
|
||||||
|
desktopHardwareFallbackUntil atomic.Int64
|
||||||
h264WarnOnce sync.Once
|
h264WarnOnce sync.Once
|
||||||
|
backstageH264WarnOnce sync.Once
|
||||||
hevcWarnOnce sync.Once
|
hevcWarnOnce sync.Once
|
||||||
codecLogOnce sync.Once
|
codecLogOnce sync.Once
|
||||||
|
|
||||||
@@ -645,7 +651,7 @@ func ResetPrevbackstage() {
|
|||||||
|
|
||||||
func RequestDesktopFullFrame() {
|
func RequestDesktopFullFrame() {
|
||||||
requestFullFrames(2)
|
requestFullFrames(2)
|
||||||
webrtcpub.RequestKeyframe()
|
webrtcpub.RequestKeyframe(webrtcpub.KindDesktop)
|
||||||
RequestDesktopH264Keyframe()
|
RequestDesktopH264Keyframe()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -653,13 +659,27 @@ func SetDesktopSoftwareH264(enabled bool) {
|
|||||||
if desktopSoftwareH264.Swap(enabled) == enabled {
|
if desktopSoftwareH264.Swap(enabled) == enabled {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
desktopHardwareFallbackUntil.Store(0)
|
||||||
resetH264Encoder()
|
resetH264Encoder()
|
||||||
requestFullFrames(2)
|
requestFullFrames(2)
|
||||||
log.Printf("capture: desktop software h264 %v", enabled)
|
log.Printf("capture: desktop software h264 %v", enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
func useDesktopSoftwareH264() bool {
|
func useDesktopSoftwareH264() bool {
|
||||||
return desktopSoftwareH264.Load()
|
if desktopSoftwareH264.Load() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
until := desktopHardwareFallbackUntil.Load()
|
||||||
|
return until > 0 && time.Now().UnixNano() < until
|
||||||
|
}
|
||||||
|
|
||||||
|
func suppressDesktopHardwareH264(err error) {
|
||||||
|
until := time.Now().Add(desktopHardwareH264FallbackCooldown).UnixNano()
|
||||||
|
previous := desktopHardwareFallbackUntil.Swap(until)
|
||||||
|
resetH264Encoder()
|
||||||
|
if previous < time.Now().UnixNano() {
|
||||||
|
log.Printf("capture: hardware H.264 temporarily unavailable (%v); using software H.264 for %s", err, desktopHardwareH264FallbackCooldown)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func RequestbackstageFullFrame() {
|
func RequestbackstageFullFrame() {
|
||||||
@@ -667,12 +687,19 @@ func RequestbackstageFullFrame() {
|
|||||||
backstagePrevFrame = nil
|
backstagePrevFrame = nil
|
||||||
backstagePrevMu.Unlock()
|
backstagePrevMu.Unlock()
|
||||||
backstageLastKeyframe.Store(0)
|
backstageLastKeyframe.Store(0)
|
||||||
resetH264Encoderbackstage()
|
RequestBackstageH264Keyframe()
|
||||||
}
|
}
|
||||||
|
|
||||||
func jpegQuality() int {
|
func desktopJPEGQuality() int {
|
||||||
|
return streamJPEGQuality(&desktopOverrideQuality)
|
||||||
|
}
|
||||||
|
|
||||||
if q := overrideQuality.Load(); q > 0 {
|
func backstageJPEGQuality() int {
|
||||||
|
return streamJPEGQuality(&backstageOverrideQuality)
|
||||||
|
}
|
||||||
|
|
||||||
|
func streamJPEGQuality(override *atomic.Int64) int {
|
||||||
|
if q := override.Load(); q > 0 {
|
||||||
return int(q)
|
return int(q)
|
||||||
}
|
}
|
||||||
q := int(loadOnceInt(&cachedJPEGQuality, 95))
|
q := int(loadOnceInt(&cachedJPEGQuality, 95))
|
||||||
@@ -761,13 +788,13 @@ func buildFrame(img *image.RGBA, display int, quality int) (wire.Frame, time.Dur
|
|||||||
height := img.Bounds().Dy()
|
height := img.Bounds().Dy()
|
||||||
full := false
|
full := false
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
codec := blockCodec()
|
codec := desktopCodec()
|
||||||
if codec == "hevc" {
|
if codec == "hevc" {
|
||||||
if width%2 != 0 || height%2 != 0 {
|
if width%2 != 0 || height%2 != 0 {
|
||||||
log.Printf("capture: hevc skipped for odd dimensions (%dx%d), falling back to h264/jpeg", width, height)
|
log.Printf("capture: hevc skipped for odd dimensions (%dx%d), falling back to h264/jpeg", width, height)
|
||||||
codec = "h264"
|
codec = "h264"
|
||||||
} else {
|
} else {
|
||||||
if webrtcpub.ConsumeKeyframeRequest() {
|
if webrtcpub.ConsumeKeyframeRequest(webrtcpub.KindDesktop) {
|
||||||
resetHEVCEncoder()
|
resetHEVCEncoder()
|
||||||
}
|
}
|
||||||
hevcBytes, err := encodeHEVCFrame(img)
|
hevcBytes, err := encodeHEVCFrame(img)
|
||||||
@@ -782,10 +809,10 @@ func buildFrame(img *image.RGBA, display int, quality int) (wire.Frame, time.Dur
|
|||||||
resetHEVCEncoder()
|
resetHEVCEncoder()
|
||||||
if h264Available() {
|
if h264Available() {
|
||||||
codec = "h264"
|
codec = "h264"
|
||||||
overrideCodec.Store("h264")
|
desktopOverrideCodec.Store("h264")
|
||||||
} else {
|
} else {
|
||||||
codec = "jpeg"
|
codec = "jpeg"
|
||||||
overrideCodec.Store("jpeg")
|
desktopOverrideCodec.Store("jpeg")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -795,7 +822,7 @@ func buildFrame(img *image.RGBA, display int, quality int) (wire.Frame, time.Dur
|
|||||||
log.Printf("capture: h264 skipped for odd dimensions (%dx%d), falling back to jpeg", width, height)
|
log.Printf("capture: h264 skipped for odd dimensions (%dx%d), falling back to jpeg", width, height)
|
||||||
codec = "jpeg"
|
codec = "jpeg"
|
||||||
} else {
|
} else {
|
||||||
if webrtcpub.ConsumeKeyframeRequest() {
|
if webrtcpub.ConsumeKeyframeRequest(webrtcpub.KindDesktop) {
|
||||||
resetH264Encoder()
|
resetH264Encoder()
|
||||||
}
|
}
|
||||||
h264Bytes, err := encodeH264Frame(img)
|
h264Bytes, err := encodeH264Frame(img)
|
||||||
@@ -892,7 +919,7 @@ func buildFramebackstage(img *image.RGBA, display int, quality int) (wire.Frame,
|
|||||||
encStart := time.Now()
|
encStart := time.Now()
|
||||||
width := img.Bounds().Dx()
|
width := img.Bounds().Dx()
|
||||||
height := img.Bounds().Dy()
|
height := img.Bounds().Dy()
|
||||||
codec := blockCodec()
|
codec := backstageCodec()
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
if codec == "h264" {
|
if codec == "h264" {
|
||||||
@@ -900,7 +927,7 @@ func buildFramebackstage(img *image.RGBA, display int, quality int) (wire.Frame,
|
|||||||
log.Printf("backstage capture: h264 skipped for odd dimensions (%dx%d), falling back to jpeg", width, height)
|
log.Printf("backstage capture: h264 skipped for odd dimensions (%dx%d), falling back to jpeg", width, height)
|
||||||
codec = "jpeg"
|
codec = "jpeg"
|
||||||
} else {
|
} else {
|
||||||
if webrtcpub.ConsumeKeyframeRequest() {
|
if webrtcpub.ConsumeKeyframeRequest(webrtcpub.Kindbackstage) {
|
||||||
resetH264Encoderbackstage()
|
resetH264Encoderbackstage()
|
||||||
}
|
}
|
||||||
h264Bytes, err := encodeH264Framebackstage(img)
|
h264Bytes, err := encodeH264Framebackstage(img)
|
||||||
@@ -910,7 +937,7 @@ func buildFramebackstage(img *image.RGBA, display int, quality int) (wire.Frame,
|
|||||||
frame := wire.Frame{Type: "frame", Header: wire.FrameHeader{Monitor: display, FPS: 0, Format: "h264", Backstage: true}, Data: h264Bytes}
|
frame := wire.Frame{Type: "frame", Header: wire.FrameHeader{Monitor: display, FPS: 0, Format: "h264", Backstage: true}, Data: h264Bytes}
|
||||||
return frame, time.Since(encStart), nil
|
return frame, time.Since(encStart), nil
|
||||||
}
|
}
|
||||||
h264WarnOnce.Do(func() {
|
backstageH264WarnOnce.Do(func() {
|
||||||
detail := h264AvailabilityDetail()
|
detail := h264AvailabilityDetail()
|
||||||
if detail != "" {
|
if detail != "" {
|
||||||
log.Printf("backstage capture: h264 encode unavailable, falling back to jpeg: %v (%s)", err, detail)
|
log.Printf("backstage capture: h264 encode unavailable, falling back to jpeg: %v (%s)", err, detail)
|
||||||
@@ -1589,12 +1616,7 @@ func encodeBlockRaw(img *image.RGBA, x, y, w, h int) []byte {
|
|||||||
return buf
|
return buf
|
||||||
}
|
}
|
||||||
|
|
||||||
func blockCodec() string {
|
func defaultCodec() string {
|
||||||
if v := overrideCodec.Load(); v != nil {
|
|
||||||
if s, ok := v.(string); ok && s != "" {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
}
|
|
||||||
blockCodecOnce.Do(func() {
|
blockCodecOnce.Do(func() {
|
||||||
codec := strings.ToLower(os.Getenv(blockCodecEnv))
|
codec := strings.ToLower(os.Getenv(blockCodecEnv))
|
||||||
switch codec {
|
switch codec {
|
||||||
@@ -1613,12 +1635,33 @@ func blockCodec() string {
|
|||||||
return cachedBlockCodec
|
return cachedBlockCodec
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetQualityAndCodec(quality int, codec string) {
|
func selectedCodec(override *atomic.Value) string {
|
||||||
|
if v := override.Load(); v != nil {
|
||||||
|
if s, ok := v.(string); ok && s != "" {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultCodec()
|
||||||
|
}
|
||||||
|
|
||||||
|
func desktopCodec() string {
|
||||||
|
return selectedCodec(&desktopOverrideCodec)
|
||||||
|
}
|
||||||
|
|
||||||
|
func backstageCodec() string {
|
||||||
|
codec := selectedCodec(&backstageOverrideCodec)
|
||||||
|
if codec == "hevc" {
|
||||||
|
return "jpeg"
|
||||||
|
}
|
||||||
|
return codec
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetDesktopQualityAndCodec(quality int, codec string) {
|
||||||
if quality > 0 {
|
if quality > 0 {
|
||||||
if quality > 100 {
|
if quality > 100 {
|
||||||
quality = 100
|
quality = 100
|
||||||
}
|
}
|
||||||
overrideQuality.Store(int64(quality))
|
desktopOverrideQuality.Store(int64(quality))
|
||||||
}
|
}
|
||||||
s := strings.ToLower(strings.TrimSpace(codec))
|
s := strings.ToLower(strings.TrimSpace(codec))
|
||||||
if s == "h265" {
|
if s == "h265" {
|
||||||
@@ -1630,40 +1673,60 @@ func SetQualityAndCodec(quality int, codec string) {
|
|||||||
if h264Available() {
|
if h264Available() {
|
||||||
fallback = "h264"
|
fallback = "h264"
|
||||||
}
|
}
|
||||||
log.Printf("capture: requested codec=hevc but unavailable (%s); forcing codec=%s", detail, fallback)
|
log.Printf("capture: requested desktop codec=hevc but unavailable (%s); forcing codec=%s", detail, fallback)
|
||||||
s = fallback
|
s = fallback
|
||||||
}
|
}
|
||||||
if s == "h264" && !h264Available() {
|
if s == "h264" && !h264Available() {
|
||||||
detail := h264AvailabilityDetail()
|
detail := h264AvailabilityDetail()
|
||||||
if detail != "" {
|
log.Printf("capture: requested desktop codec=h264 but unavailable (%s); forcing codec=jpeg", detail)
|
||||||
log.Printf("capture: requested codec=h264 but unavailable (%s); forcing codec=jpeg", detail)
|
|
||||||
} else {
|
|
||||||
log.Printf("capture: requested codec=h264 but unavailable; forcing codec=jpeg")
|
|
||||||
}
|
|
||||||
s = "jpeg"
|
s = "jpeg"
|
||||||
}
|
}
|
||||||
switch s {
|
switch s {
|
||||||
case "raw", "rgba", "jpeg", "h264", "hevc":
|
case "raw", "rgba", "jpeg", "h264", "hevc":
|
||||||
overrideCodec.Store(s)
|
desktopOverrideCodec.Store(s)
|
||||||
h264WarnOnce = sync.Once{}
|
h264WarnOnce = sync.Once{}
|
||||||
hevcWarnOnce = sync.Once{}
|
hevcWarnOnce = sync.Once{}
|
||||||
if s != "h264" {
|
if s != "h264" {
|
||||||
resetH264Encoder()
|
resetH264Encoder()
|
||||||
resetH264Encoderbackstage()
|
|
||||||
}
|
}
|
||||||
if s != "hevc" {
|
if s != "hevc" {
|
||||||
resetHEVCEncoder()
|
resetHEVCEncoder()
|
||||||
}
|
}
|
||||||
case "":
|
case "":
|
||||||
|
|
||||||
default:
|
default:
|
||||||
overrideCodec.Store("jpeg")
|
desktopOverrideCodec.Store("jpeg")
|
||||||
resetH264Encoder()
|
resetH264Encoder()
|
||||||
resetH264Encoderbackstage()
|
|
||||||
resetHEVCEncoder()
|
resetHEVCEncoder()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SetBackstageQualityAndCodec(quality int, codec string) {
|
||||||
|
if quality > 0 {
|
||||||
|
if quality > 100 {
|
||||||
|
quality = 100
|
||||||
|
}
|
||||||
|
backstageOverrideQuality.Store(int64(quality))
|
||||||
|
}
|
||||||
|
s := strings.ToLower(strings.TrimSpace(codec))
|
||||||
|
if s == "h264" && !h264Available() {
|
||||||
|
detail := h264AvailabilityDetail()
|
||||||
|
log.Printf("capture: requested backstage codec=h264 but unavailable (%s); forcing codec=jpeg", detail)
|
||||||
|
s = "jpeg"
|
||||||
|
}
|
||||||
|
switch s {
|
||||||
|
case "raw", "rgba", "jpeg", "h264":
|
||||||
|
backstageOverrideCodec.Store(s)
|
||||||
|
backstageH264WarnOnce = sync.Once{}
|
||||||
|
if s != "h264" {
|
||||||
|
resetH264Encoderbackstage()
|
||||||
|
}
|
||||||
|
case "":
|
||||||
|
default:
|
||||||
|
backstageOverrideCodec.Store("jpeg")
|
||||||
|
resetH264Encoderbackstage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func Nowbackstage(ctx context.Context, env *rt.Env) error {
|
func Nowbackstage(ctx context.Context, env *rt.Env) error {
|
||||||
if env.Cfg.DisableCapture {
|
if env.Cfg.DisableCapture {
|
||||||
return sendBlackFramebackstage(ctx, env)
|
return sendBlackFramebackstage(ctx, env)
|
||||||
@@ -1725,7 +1788,7 @@ func captureAndSendVirtual(ctx context.Context, env *rt.Env) error {
|
|||||||
}
|
}
|
||||||
captureDur := time.Since(t0)
|
captureDur := time.Since(t0)
|
||||||
|
|
||||||
willSendViaWebRTC := blockCodec() == "h264" && webrtcpub.IsActive(webrtcpub.Kindbackstage)
|
willSendViaWebRTC := backstageCodec() == "h264" && webrtcpub.IsActive(webrtcpub.Kindbackstage)
|
||||||
var slotAcquired bool
|
var slotAcquired bool
|
||||||
if !willSendViaWebRTC && !AcquireFrameSlot() {
|
if !willSendViaWebRTC && !AcquireFrameSlot() {
|
||||||
PutRGBA(img)
|
PutRGBA(img)
|
||||||
@@ -1733,7 +1796,7 @@ func captureAndSendVirtual(ctx context.Context, env *rt.Env) error {
|
|||||||
}
|
}
|
||||||
slotAcquired = !willSendViaWebRTC
|
slotAcquired = !willSendViaWebRTC
|
||||||
|
|
||||||
quality := jpegQuality()
|
quality := backstageJPEGQuality()
|
||||||
frame, encodeDur, err := buildFramebackstage(img, 0, quality)
|
frame, encodeDur, err := buildFramebackstage(img, 0, quality)
|
||||||
PutRGBA(img)
|
PutRGBA(img)
|
||||||
img = nil
|
img = nil
|
||||||
@@ -1873,7 +1936,7 @@ func captureAndSendbackstage(ctx context.Context, env *rt.Env) error {
|
|||||||
}
|
}
|
||||||
captureDur := time.Since(t0)
|
captureDur := time.Since(t0)
|
||||||
|
|
||||||
willSendViaWebRTC := blockCodec() == "h264" && webrtcpub.IsActive(webrtcpub.Kindbackstage)
|
willSendViaWebRTC := backstageCodec() == "h264" && webrtcpub.IsActive(webrtcpub.Kindbackstage)
|
||||||
var slotAcquired bool
|
var slotAcquired bool
|
||||||
if !willSendViaWebRTC && !AcquireFrameSlot() {
|
if !willSendViaWebRTC && !AcquireFrameSlot() {
|
||||||
PutRGBA(img)
|
PutRGBA(img)
|
||||||
@@ -1881,7 +1944,7 @@ func captureAndSendbackstage(ctx context.Context, env *rt.Env) error {
|
|||||||
}
|
}
|
||||||
slotAcquired = !willSendViaWebRTC
|
slotAcquired = !willSendViaWebRTC
|
||||||
|
|
||||||
quality := jpegQuality()
|
quality := backstageJPEGQuality()
|
||||||
frame, encodeDur, err := buildFramebackstage(img, display, quality)
|
frame, encodeDur, err := buildFramebackstage(img, display, quality)
|
||||||
PutRGBA(img)
|
PutRGBA(img)
|
||||||
img = nil
|
img = nil
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ func RunCaptureSmoke(opts CaptureSmokeOptions) []CaptureSmokeResult {
|
|||||||
opts.Display = 0
|
opts.Display = 0
|
||||||
}
|
}
|
||||||
if opts.FPS > 0 {
|
if opts.FPS > 0 {
|
||||||
SetH264TargetFPS(opts.FPS)
|
SetDesktopH264TargetFPS(opts.FPS)
|
||||||
}
|
}
|
||||||
if opts.MaxHeight != 0 {
|
if opts.MaxHeight != 0 {
|
||||||
SetMaxResolution(opts.MaxHeight)
|
SetMaxResolution(opts.MaxHeight)
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ func TestActualScreenCapture(t *testing.T) {
|
|||||||
t.Log("WARNING: Captured image appears to be solid color (may indicate capture issue)")
|
t.Log("WARNING: Captured image appears to be solid color (may indicate capture issue)")
|
||||||
}
|
}
|
||||||
|
|
||||||
quality := jpegQuality()
|
quality := desktopJPEGQuality()
|
||||||
frame, encodeDur, err := buildFrame(img, 0, quality)
|
frame, encodeDur, err := buildFrame(img, 0, quality)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Failed to build frame from captured image: %v", err)
|
t.Errorf("Failed to build frame from captured image: %v", err)
|
||||||
@@ -270,7 +270,7 @@ func TestFrameFPS(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestJPEGQuality(t *testing.T) {
|
func TestJPEGQuality(t *testing.T) {
|
||||||
quality := jpegQuality()
|
quality := desktopJPEGQuality()
|
||||||
if quality < 1 || quality > 100 {
|
if quality < 1 || quality > 100 {
|
||||||
t.Errorf("JPEG quality out of range: %d (expected 1-100)", quality)
|
t.Errorf("JPEG quality out of range: %d (expected 1-100)", quality)
|
||||||
}
|
}
|
||||||
@@ -479,11 +479,11 @@ func TestSetFrameFlowTargetFPSEnvOverride(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRequestDesktopFullFrameRequestsWebRTCKeyframe(t *testing.T) {
|
func TestRequestDesktopFullFrameRequestsWebRTCKeyframe(t *testing.T) {
|
||||||
_ = webrtcpub.ConsumeKeyframeRequest()
|
_ = webrtcpub.ConsumeKeyframeRequest(webrtcpub.KindDesktop)
|
||||||
|
|
||||||
RequestDesktopFullFrame()
|
RequestDesktopFullFrame()
|
||||||
|
|
||||||
if !webrtcpub.ConsumeKeyframeRequest() {
|
if !webrtcpub.ConsumeKeyframeRequest(webrtcpub.KindDesktop) {
|
||||||
t.Fatal("expected desktop full-frame request to force the next WebRTC video frame to be a keyframe")
|
t.Fatal("expected desktop full-frame request to force the next WebRTC video frame to be a keyframe")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,5 +3,5 @@
|
|||||||
package capture
|
package capture
|
||||||
|
|
||||||
func resetH264TextureEncoderForBitrate() {
|
func resetH264TextureEncoderForBitrate() {
|
||||||
resetH264D3D11TextureEncoder()
|
resetAllH264D3D11TextureEncoders()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package capture
|
package capture
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
"os"
|
"os"
|
||||||
@@ -12,14 +14,17 @@ import (
|
|||||||
func resetCodecSelectionForTest() {
|
func resetCodecSelectionForTest() {
|
||||||
blockCodecOnce = sync.Once{}
|
blockCodecOnce = sync.Once{}
|
||||||
cachedBlockCodec = ""
|
cachedBlockCodec = ""
|
||||||
overrideCodec.Store("")
|
desktopOverrideCodec.Store("")
|
||||||
|
backstageOverrideCodec.Store("")
|
||||||
|
desktopOverrideQuality.Store(0)
|
||||||
|
backstageOverrideQuality.Store(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSetQualityAndCodec_H264FallbackDependsOnAvailability(t *testing.T) {
|
func TestSetQualityAndCodec_H264FallbackDependsOnAvailability(t *testing.T) {
|
||||||
t.Cleanup(resetCodecSelectionForTest)
|
t.Cleanup(resetCodecSelectionForTest)
|
||||||
|
|
||||||
SetQualityAndCodec(80, "h264")
|
SetDesktopQualityAndCodec(80, "h264")
|
||||||
got := blockCodec()
|
got := desktopCodec()
|
||||||
|
|
||||||
if h264Available() {
|
if h264Available() {
|
||||||
if got != "h264" {
|
if got != "h264" {
|
||||||
@@ -36,13 +41,56 @@ func TestSetQualityAndCodec_H264FallbackDependsOnAvailability(t *testing.T) {
|
|||||||
func TestSetQualityAndCodec_InvalidCodecForcesJpeg(t *testing.T) {
|
func TestSetQualityAndCodec_InvalidCodecForcesJpeg(t *testing.T) {
|
||||||
t.Cleanup(resetCodecSelectionForTest)
|
t.Cleanup(resetCodecSelectionForTest)
|
||||||
|
|
||||||
SetQualityAndCodec(75, "invalid-codec")
|
SetDesktopQualityAndCodec(75, "invalid-codec")
|
||||||
got := blockCodec()
|
got := desktopCodec()
|
||||||
if got != "jpeg" {
|
if got != "jpeg" {
|
||||||
t.Fatalf("expected invalid codec to force jpeg, got %q", got)
|
t.Fatalf("expected invalid codec to force jpeg, got %q", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDesktopAndBackstageCodecSelectionsAreIndependent(t *testing.T) {
|
||||||
|
t.Cleanup(resetCodecSelectionForTest)
|
||||||
|
|
||||||
|
SetDesktopQualityAndCodec(80, "h264")
|
||||||
|
SetBackstageQualityAndCodec(65, "jpeg")
|
||||||
|
|
||||||
|
wantDesktop := "jpeg"
|
||||||
|
if h264Available() {
|
||||||
|
wantDesktop = "h264"
|
||||||
|
}
|
||||||
|
if got := desktopCodec(); got != wantDesktop {
|
||||||
|
t.Fatalf("desktop codec = %q, want %q after backstage update", got, wantDesktop)
|
||||||
|
}
|
||||||
|
if got := backstageCodec(); got != "jpeg" {
|
||||||
|
t.Fatalf("backstage codec = %q, want jpeg", got)
|
||||||
|
}
|
||||||
|
if got := desktopJPEGQuality(); got != 80 {
|
||||||
|
t.Fatalf("desktop quality = %d, want 80", got)
|
||||||
|
}
|
||||||
|
if got := backstageJPEGQuality(); got != 65 {
|
||||||
|
t.Fatalf("backstage quality = %d, want 65", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDesktopHardwareFailureDoesNotChangeBackstageCodec(t *testing.T) {
|
||||||
|
t.Cleanup(func() {
|
||||||
|
desktopHardwareFallbackUntil.Store(0)
|
||||||
|
resetCodecSelectionForTest()
|
||||||
|
})
|
||||||
|
SetDesktopQualityAndCodec(80, "h264")
|
||||||
|
SetBackstageQualityAndCodec(65, "h264")
|
||||||
|
before := backstageCodec()
|
||||||
|
|
||||||
|
suppressDesktopHardwareH264(errors.New("NVENC session failure"))
|
||||||
|
|
||||||
|
if !useDesktopSoftwareH264() {
|
||||||
|
t.Fatal("desktop did not enter temporary software H.264 fallback")
|
||||||
|
}
|
||||||
|
if got := backstageCodec(); got != before {
|
||||||
|
t.Fatalf("backstage codec changed from %q to %q after desktop encoder failure", before, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestH264AvailabilityDetail_NotEmpty(t *testing.T) {
|
func TestH264AvailabilityDetail_NotEmpty(t *testing.T) {
|
||||||
detail := strings.TrimSpace(h264AvailabilityDetail())
|
detail := strings.TrimSpace(h264AvailabilityDetail())
|
||||||
if detail == "" {
|
if detail == "" {
|
||||||
@@ -93,6 +141,59 @@ func TestEncodeH264Frame_WhenAvailableProducesBytes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDesktopAndBackstageH264EncodeConcurrently(t *testing.T) {
|
||||||
|
if !h264Available() {
|
||||||
|
t.Skip("h264 is unavailable in this build")
|
||||||
|
}
|
||||||
|
SetDesktopH264TargetFPS(60)
|
||||||
|
SetBackstageH264TargetFPS(30)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
resetH264Encoder()
|
||||||
|
resetH264Encoderbackstage()
|
||||||
|
})
|
||||||
|
|
||||||
|
const width, height = 640, 360
|
||||||
|
start := make(chan struct{})
|
||||||
|
results := make(chan error, 2)
|
||||||
|
run := func(name string, encode func(*image.RGBA) ([]byte, error), seed uint8) {
|
||||||
|
<-start
|
||||||
|
img := image.NewRGBA(image.Rect(0, 0, width, height))
|
||||||
|
produced := false
|
||||||
|
for frame := range 12 {
|
||||||
|
for y := range height {
|
||||||
|
for x := range width {
|
||||||
|
img.SetRGBA(x, y, color.RGBA{
|
||||||
|
R: uint8(x*3+frame*7) + seed,
|
||||||
|
G: uint8(y*3+frame*5) + seed,
|
||||||
|
B: uint8(frame*11) + seed,
|
||||||
|
A: 255,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out, err := encode(img)
|
||||||
|
if err != nil {
|
||||||
|
results <- fmt.Errorf("%s encode frame %d: %w", name, frame, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
produced = produced || len(out) > 0
|
||||||
|
}
|
||||||
|
if !produced {
|
||||||
|
results <- fmt.Errorf("%s produced no H.264 output", name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
results <- nil
|
||||||
|
}
|
||||||
|
|
||||||
|
go run("desktop", encodeH264Frame, 17)
|
||||||
|
go run("backstage", encodeH264Framebackstage, 83)
|
||||||
|
close(start)
|
||||||
|
for range 2 {
|
||||||
|
if err := <-results; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestBlockCodec_UsesEnvWhenNoOverride(t *testing.T) {
|
func TestBlockCodec_UsesEnvWhenNoOverride(t *testing.T) {
|
||||||
prev := os.Getenv(blockCodecEnv)
|
prev := os.Getenv(blockCodecEnv)
|
||||||
if err := os.Setenv(blockCodecEnv, "raw"); err != nil {
|
if err := os.Setenv(blockCodecEnv, "raw"); err != nil {
|
||||||
@@ -104,7 +205,7 @@ func TestBlockCodec_UsesEnvWhenNoOverride(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
resetCodecSelectionForTest()
|
resetCodecSelectionForTest()
|
||||||
got := blockCodec()
|
got := desktopCodec()
|
||||||
if got != "raw" {
|
if got != "raw" {
|
||||||
t.Fatalf("expected env codec raw, got %q", got)
|
t.Fatalf("expected env codec raw, got %q", got)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,13 +17,13 @@ func directVideoKeyframeDue(requested bool, nowNs, lastKeyframeNs int64) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func tryBuildDirectH264Frame(display int) (wire.Frame, time.Duration, time.Duration, bool, error) {
|
func tryBuildDirectH264Frame(display int) (wire.Frame, time.Duration, time.Duration, bool, error) {
|
||||||
codec := blockCodec()
|
codec := desktopCodec()
|
||||||
if (codec != "h264" && codec != "hevc") || (codec == "h264" && useDesktopSoftwareH264()) || !useDesktopDuplication() {
|
if (codec != "h264" && codec != "hevc") || (codec == "h264" && useDesktopSoftwareH264()) || !useDesktopDuplication() {
|
||||||
return wire.Frame{}, 0, 0, false, nil
|
return wire.Frame{}, 0, 0, false, nil
|
||||||
}
|
}
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
forceKeyframe := directVideoKeyframeDue(
|
forceKeyframe := directVideoKeyframeDue(
|
||||||
webrtcpub.ConsumeKeyframeRequest(),
|
webrtcpub.ConsumeKeyframeRequest(webrtcpub.KindDesktop),
|
||||||
now.UnixNano(),
|
now.UnixNano(),
|
||||||
lastKeyframe.Load(),
|
lastKeyframe.Load(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ func resetH264Encoder() {}
|
|||||||
func RequestDesktopH264Keyframe() {}
|
func RequestDesktopH264Keyframe() {}
|
||||||
|
|
||||||
func resetH264Encoderbackstage() {}
|
func resetH264Encoderbackstage() {}
|
||||||
|
func RequestBackstageH264Keyframe() {}
|
||||||
|
|
||||||
func encodeH264Framebackstage(_ *image.RGBA) ([]byte, error) {
|
func encodeH264Framebackstage(_ *image.RGBA) ([]byte, error) {
|
||||||
return nil, errors.New("h264 support not available (cgo disabled)")
|
return nil, errors.New("h264 support not available (cgo disabled)")
|
||||||
@@ -33,4 +34,6 @@ func encodeH264FrameWebcam(_ *image.RGBA) ([]byte, error) {
|
|||||||
return nil, errors.New("h264 support not available (cgo disabled)")
|
return nil, errors.New("h264 support not available (cgo disabled)")
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetH264TargetFPS(_ int) {}
|
func SetDesktopH264TargetFPS(_ int) {}
|
||||||
|
func SetBackstageH264TargetFPS(_ int) {}
|
||||||
|
func SetWebcamH264TargetFPS(_ int) {}
|
||||||
|
|||||||
@@ -519,7 +519,9 @@ type mfH264Candidate struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
h264TargetFPS atomic.Int64
|
desktopH264TargetFPS atomic.Int64
|
||||||
|
backstageH264TargetFPS atomic.Int64
|
||||||
|
webcamH264TargetFPS atomic.Int64
|
||||||
|
|
||||||
h264Mu sync.Mutex
|
h264Mu sync.Mutex
|
||||||
h264Enc h264FrameEncoder
|
h264Enc h264FrameEncoder
|
||||||
@@ -565,7 +567,7 @@ func encodeH264FrameWithEncoder(slot *h264FrameEncoder, stream string, img *imag
|
|||||||
return nil, fmt.Errorf("h264 frame size must be even, got %dx%d", width, height)
|
return nil, fmt.Errorf("h264 frame size must be even, got %dx%d", width, height)
|
||||||
}
|
}
|
||||||
|
|
||||||
fps := activeH264FPS()
|
fps := activeH264FPSForStream(stream)
|
||||||
if *slot == nil || !(*slot).Matches(width, height, fps) {
|
if *slot == nil || !(*slot).Matches(width, height, fps) {
|
||||||
if *slot != nil {
|
if *slot != nil {
|
||||||
(*slot).Close()
|
(*slot).Close()
|
||||||
@@ -577,7 +579,39 @@ func encodeH264FrameWithEncoder(slot *h264FrameEncoder, stream string, img *imag
|
|||||||
}
|
}
|
||||||
*slot = enc
|
*slot = enc
|
||||||
}
|
}
|
||||||
return (*slot).Encode(img)
|
return encodeH264FrameWithRuntimeFallback(slot, stream, img, fps, newSoftwareH264Fallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
type h264SoftwareFallbackFactory func(width, height, fps int) (h264FrameEncoder, error)
|
||||||
|
|
||||||
|
func newSoftwareH264Fallback(width, height, fps int) (h264FrameEncoder, error) {
|
||||||
|
return newMFSoftwareH264Encoder(width, height, fps)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeH264FrameWithRuntimeFallback(
|
||||||
|
slot *h264FrameEncoder,
|
||||||
|
stream string,
|
||||||
|
img *image.RGBA,
|
||||||
|
fps int,
|
||||||
|
createSoftware h264SoftwareFallbackFactory,
|
||||||
|
) ([]byte, error) {
|
||||||
|
out, err := (*slot).Encode(img)
|
||||||
|
if err == nil {
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
hardwareErr := err
|
||||||
|
closeH264Encoder(slot)
|
||||||
|
fallback, fallbackErr := createSoftware(img.Bounds().Dx(), img.Bounds().Dy(), fps)
|
||||||
|
if fallbackErr != nil {
|
||||||
|
return nil, fmt.Errorf("%s h264 encoder failed (%v) and software fallback could not start: %w", stream, hardwareErr, fallbackErr)
|
||||||
|
}
|
||||||
|
*slot = fallback
|
||||||
|
log.Printf("capture: %s h264 encoder failed at runtime: %v; switched this stream to Media Foundation software H.264", stream, hardwareErr)
|
||||||
|
out, err = fallback.Encode(img)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s software h264 fallback encode failed after hardware error %v: %w", stream, hardwareErr, err)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func h264Available() bool {
|
func h264Available() bool {
|
||||||
@@ -601,18 +635,45 @@ func h264AvailabilityDetail() string {
|
|||||||
return "Windows Media Foundation H.264 MFT (Microsoft H.264 encoder fallback; hardware unavailable: " + detail + ")"
|
return "Windows Media Foundation H.264 MFT (Microsoft H.264 encoder fallback; hardware unavailable: " + detail + ")"
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetH264TargetFPS(fps int) {
|
func SetDesktopH264TargetFPS(fps int) {
|
||||||
|
setH264TargetFPS(&desktopH264TargetFPS, fps)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetBackstageH264TargetFPS(fps int) {
|
||||||
|
setH264TargetFPS(&backstageH264TargetFPS, fps)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetWebcamH264TargetFPS(fps int) {
|
||||||
|
setH264TargetFPS(&webcamH264TargetFPS, fps)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setH264TargetFPS(target *atomic.Int64, fps int) {
|
||||||
if fps < 1 {
|
if fps < 1 {
|
||||||
fps = 1
|
fps = 1
|
||||||
}
|
}
|
||||||
h264TargetFPS.Store(int64(fps))
|
target.Store(int64(fps))
|
||||||
}
|
}
|
||||||
|
|
||||||
func activeH264FPS() int {
|
func activeH264FPS() int {
|
||||||
if v := int(h264TargetFPS.Load()); v > 0 {
|
return activeH264FPSValue(&desktopH264TargetFPS, 60)
|
||||||
|
}
|
||||||
|
|
||||||
|
func activeH264FPSForStream(stream string) int {
|
||||||
|
switch stream {
|
||||||
|
case "backstage":
|
||||||
|
return activeH264FPSValue(&backstageH264TargetFPS, 60)
|
||||||
|
case "webcam":
|
||||||
|
return activeH264FPSValue(&webcamH264TargetFPS, 30)
|
||||||
|
default:
|
||||||
|
return activeH264FPS()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func activeH264FPSValue(target *atomic.Int64, fallback int) int {
|
||||||
|
if v := int(target.Load()); v > 0 {
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
return 60
|
return fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
func resetH264Encoder() {
|
func resetH264Encoder() {
|
||||||
@@ -622,7 +683,7 @@ func resetH264Encoder() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func RequestDesktopH264Keyframe() {
|
func RequestDesktopH264Keyframe() {
|
||||||
requestH264D3D11TextureKeyframe()
|
requestH264D3D11TextureKeyframe("desktop")
|
||||||
resetH264Encoder()
|
resetH264Encoder()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -632,6 +693,11 @@ func resetH264Encoderbackstage() {
|
|||||||
closeH264Encoder(&backstageH264Enc)
|
closeH264Encoder(&backstageH264Enc)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func RequestBackstageH264Keyframe() {
|
||||||
|
requestH264D3D11TextureKeyframe("backstage")
|
||||||
|
resetH264Encoderbackstage()
|
||||||
|
}
|
||||||
|
|
||||||
func closeH264Encoder(slot *h264FrameEncoder) {
|
func closeH264Encoder(slot *h264FrameEncoder) {
|
||||||
if slot != nil && *slot != nil {
|
if slot != nil && *slot != nil {
|
||||||
(*slot).Close()
|
(*slot).Close()
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ var (
|
|||||||
h264FPS int
|
h264FPS int
|
||||||
h264LastErr error
|
h264LastErr error
|
||||||
h264Scratch []byte
|
h264Scratch []byte
|
||||||
h264TargetFPS atomic.Int64 // set by the stream handler; default 60
|
desktopH264TargetFPS atomic.Int64
|
||||||
|
backstageH264TargetFPS atomic.Int64
|
||||||
|
webcamH264TargetFPS atomic.Int64
|
||||||
)
|
)
|
||||||
|
|
||||||
func encodeH264Frame(img *image.RGBA) ([]byte, error) {
|
func encodeH264Frame(img *image.RGBA) ([]byte, error) {
|
||||||
@@ -70,22 +72,34 @@ func h264AvailabilityDetail() string {
|
|||||||
return "cgo build with x264-go"
|
return "cgo build with x264-go"
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetH264TargetFPS sets the frame rate hint for the x264 encoder so its
|
func SetDesktopH264TargetFPS(fps int) {
|
||||||
// rate-control matches the actual capture cadence. Call this before (or
|
setH264TargetFPS(&desktopH264TargetFPS, fps)
|
||||||
// when) streaming starts. The encoder is automatically re-created on the
|
}
|
||||||
// next frame if the value differs from the current one.
|
|
||||||
func SetH264TargetFPS(fps int) {
|
func SetBackstageH264TargetFPS(fps int) {
|
||||||
|
setH264TargetFPS(&backstageH264TargetFPS, fps)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetWebcamH264TargetFPS(fps int) {
|
||||||
|
setH264TargetFPS(&webcamH264TargetFPS, fps)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setH264TargetFPS(target *atomic.Int64, fps int) {
|
||||||
if fps < 1 {
|
if fps < 1 {
|
||||||
fps = 1
|
fps = 1
|
||||||
}
|
}
|
||||||
h264TargetFPS.Store(int64(fps))
|
target.Store(int64(fps))
|
||||||
}
|
}
|
||||||
|
|
||||||
func activeH264FPS() int {
|
func activeH264FPS() int {
|
||||||
if v := int(h264TargetFPS.Load()); v > 0 {
|
return activeH264FPSValue(&desktopH264TargetFPS, 60)
|
||||||
|
}
|
||||||
|
|
||||||
|
func activeH264FPSValue(target *atomic.Int64, fallback int) int {
|
||||||
|
if v := int(target.Load()); v > 0 {
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
return 60 // sensible default for desktop streaming
|
return fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
func ensureH264EncoderLocked(width, height int) error {
|
func ensureH264EncoderLocked(width, height int) error {
|
||||||
@@ -187,8 +201,12 @@ func resetH264Encoderbackstage() {
|
|||||||
closebackstageH264EncoderLocked()
|
closebackstageH264EncoderLocked()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func RequestBackstageH264Keyframe() {
|
||||||
|
resetH264Encoderbackstage()
|
||||||
|
}
|
||||||
|
|
||||||
func ensurebackstageH264EncoderLocked(width, height int) error {
|
func ensurebackstageH264EncoderLocked(width, height int) error {
|
||||||
fps := activeH264FPS()
|
fps := activeH264FPSValue(&backstageH264TargetFPS, 60)
|
||||||
if backstageH264Enc != nil && backstageH264Width == width && backstageH264Height == height && backstageH264FPS == fps {
|
if backstageH264Enc != nil && backstageH264Width == width && backstageH264Height == height && backstageH264FPS == fps {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -267,7 +285,7 @@ func encodeH264FrameWebcam(img *image.RGBA) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ensureWebcamH264EncoderLocked(width, height int) error {
|
func ensureWebcamH264EncoderLocked(width, height int) error {
|
||||||
fps := activeH264FPS()
|
fps := activeH264FPSValue(&webcamH264TargetFPS, 30)
|
||||||
if webcamH264Enc != nil && webcamH264Width == width && webcamH264Height == height && webcamH264FPS == fps {
|
if webcamH264Enc != nil && webcamH264Width == width && webcamH264Height == height && webcamH264FPS == fps {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,90 +43,107 @@ type h264D3D11TextureBackend interface {
|
|||||||
Reset()
|
Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
type nvencD3D11TextureBackend struct{}
|
type nvencD3D11TextureBackend struct {
|
||||||
|
stream string
|
||||||
|
}
|
||||||
|
|
||||||
func (nvencD3D11TextureBackend) Name() string { return "NVIDIA NVENC" }
|
func (*nvencD3D11TextureBackend) Name() string { return "NVIDIA NVENC" }
|
||||||
|
|
||||||
func (nvencD3D11TextureBackend) Encode(req h264D3D11TextureRequest) ([]byte, error) {
|
func (b *nvencD3D11TextureBackend) Encode(req h264D3D11TextureRequest) ([]byte, error) {
|
||||||
return encodeNativeH264D3D11Texture(req.Device, req.Texture, req.InputWidth, req.InputHeight,
|
return encodeNativeH264D3D11Texture(b.stream, req.Device, req.Texture, req.InputWidth, req.InputHeight,
|
||||||
req.EncodeWidth, req.EncodeHeight, req.FPS, req.DXGIFormat, req.ForceIDR)
|
req.EncodeWidth, req.EncodeHeight, req.FPS, req.DXGIFormat, req.ForceIDR)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (nvencD3D11TextureBackend) RequestKeyframe() {
|
func (b *nvencD3D11TextureBackend) RequestKeyframe() {
|
||||||
requestNativeH264D3D11TextureKeyframe()
|
requestNativeH264D3D11TextureKeyframe(b.stream)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (nvencD3D11TextureBackend) Reset() {
|
func (b *nvencD3D11TextureBackend) Reset() {
|
||||||
resetNativeH264D3D11TextureEncoder()
|
resetNativeH264D3D11TextureEncoder(b.stream)
|
||||||
}
|
}
|
||||||
|
|
||||||
var d3d11H264TextureRegistry = struct {
|
type h264D3D11TextureRegistry struct {
|
||||||
sync.Mutex
|
sync.Mutex
|
||||||
backends []h264D3D11TextureBackend
|
backends []h264D3D11TextureBackend
|
||||||
active h264D3D11TextureBackend
|
active h264D3D11TextureBackend
|
||||||
failures map[d3d11H264FailureKey]d3d11H264Failure
|
failures map[d3d11H264FailureKey]d3d11H264Failure
|
||||||
}{
|
}
|
||||||
backends: []h264D3D11TextureBackend{nvencD3D11TextureBackend{}},
|
|
||||||
|
func newH264D3D11TextureRegistry(stream string) *h264D3D11TextureRegistry {
|
||||||
|
registry := &h264D3D11TextureRegistry{
|
||||||
|
backends: []h264D3D11TextureBackend{&nvencD3D11TextureBackend{stream: stream}},
|
||||||
failures: make(map[d3d11H264FailureKey]d3d11H264Failure),
|
failures: make(map[d3d11H264FailureKey]d3d11H264Failure),
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
|
||||||
if backend := newQSVD3D11TextureBackend(); backend != nil {
|
if backend := newQSVD3D11TextureBackend(); backend != nil {
|
||||||
d3d11H264TextureRegistry.backends = append(d3d11H264TextureRegistry.backends, backend)
|
registry.backends = append(registry.backends, backend)
|
||||||
}
|
}
|
||||||
if backend := newAMFD3D11TextureBackend(); backend != nil {
|
if backend := newAMFD3D11TextureBackend(); backend != nil {
|
||||||
d3d11H264TextureRegistry.backends = append(d3d11H264TextureRegistry.backends, backend)
|
registry.backends = append(registry.backends, backend)
|
||||||
}
|
}
|
||||||
|
return registry
|
||||||
}
|
}
|
||||||
|
|
||||||
func encodeH264D3D11Texture(req h264D3D11TextureRequest) ([]byte, string, error) {
|
var (
|
||||||
|
d3d11H264TextureRegistry = newH264D3D11TextureRegistry("desktop")
|
||||||
|
backstageD3D11H264TextureRegistry = newH264D3D11TextureRegistry("backstage")
|
||||||
|
)
|
||||||
|
|
||||||
|
func h264D3D11Registry(stream string) *h264D3D11TextureRegistry {
|
||||||
|
if stream == "backstage" {
|
||||||
|
return backstageD3D11H264TextureRegistry
|
||||||
|
}
|
||||||
|
return d3d11H264TextureRegistry
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeH264D3D11Texture(stream string, req h264D3D11TextureRequest) ([]byte, string, error) {
|
||||||
if req.Device == nil || req.Texture == nil {
|
if req.Device == nil || req.Texture == nil {
|
||||||
return nil, "", fmt.Errorf("nil D3D11 device or texture")
|
return nil, "", fmt.Errorf("nil D3D11 device or texture")
|
||||||
}
|
}
|
||||||
d3d11H264TextureRegistry.Lock()
|
registry := h264D3D11Registry(stream)
|
||||||
defer d3d11H264TextureRegistry.Unlock()
|
registry.Lock()
|
||||||
|
defer registry.Unlock()
|
||||||
|
|
||||||
failedActiveName := ""
|
failedActiveName := ""
|
||||||
errorsByBackend := make([]string, 0, len(d3d11H264TextureRegistry.backends))
|
errorsByBackend := make([]string, 0, len(registry.backends))
|
||||||
if active := d3d11H264TextureRegistry.active; active != nil {
|
if active := registry.active; active != nil {
|
||||||
key := d3d11H264FailureKeyFor(active.Name(), req)
|
key := d3d11H264FailureKeyFor(active.Name(), req)
|
||||||
if failure, failed := activeD3D11H264Failure(key, time.Now()); !failed {
|
if failure, failed := registry.activeFailure(key, time.Now()); !failed {
|
||||||
out, err := active.Encode(req)
|
out, err := active.Encode(req)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
delete(d3d11H264TextureRegistry.failures, key)
|
delete(registry.failures, key)
|
||||||
return out, active.Name(), nil
|
return out, active.Name(), nil
|
||||||
}
|
}
|
||||||
rememberD3D11H264Failure(key, err)
|
registry.rememberFailure(key, err)
|
||||||
active.Reset()
|
active.Reset()
|
||||||
failedActiveName = active.Name()
|
failedActiveName = active.Name()
|
||||||
errorsByBackend = append(errorsByBackend, active.Name()+": "+err.Error())
|
errorsByBackend = append(errorsByBackend, active.Name()+": "+err.Error())
|
||||||
d3d11H264TextureRegistry.active = nil
|
registry.active = nil
|
||||||
} else {
|
} else {
|
||||||
errorsByBackend = append(errorsByBackend, active.Name()+": "+failure.err+" (cooldown)")
|
errorsByBackend = append(errorsByBackend, active.Name()+": "+failure.err+" (cooldown)")
|
||||||
active.Reset()
|
active.Reset()
|
||||||
failedActiveName = active.Name()
|
failedActiveName = active.Name()
|
||||||
d3d11H264TextureRegistry.active = nil
|
registry.active = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
for _, backend := range d3d11H264TextureRegistry.backends {
|
for _, backend := range registry.backends {
|
||||||
if backend == nil || (failedActiveName != "" && backend.Name() == failedActiveName) {
|
if backend == nil || (failedActiveName != "" && backend.Name() == failedActiveName) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
key := d3d11H264FailureKeyFor(backend.Name(), req)
|
key := d3d11H264FailureKeyFor(backend.Name(), req)
|
||||||
if failure, failed := activeD3D11H264Failure(key, now); failed {
|
if failure, failed := registry.activeFailure(key, now); failed {
|
||||||
errorsByBackend = append(errorsByBackend, backend.Name()+": "+failure.err+" (cooldown)")
|
errorsByBackend = append(errorsByBackend, backend.Name()+": "+failure.err+" (cooldown)")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
out, err := backend.Encode(req)
|
out, err := backend.Encode(req)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
delete(d3d11H264TextureRegistry.failures, key)
|
delete(registry.failures, key)
|
||||||
d3d11H264TextureRegistry.active = backend
|
registry.active = backend
|
||||||
return out, backend.Name(), nil
|
return out, backend.Name(), nil
|
||||||
}
|
}
|
||||||
backend.Reset()
|
backend.Reset()
|
||||||
rememberD3D11H264Failure(key, err)
|
registry.rememberFailure(key, err)
|
||||||
errorsByBackend = append(errorsByBackend, backend.Name()+": "+err.Error())
|
errorsByBackend = append(errorsByBackend, backend.Name()+": "+err.Error())
|
||||||
}
|
}
|
||||||
if len(errorsByBackend) == 0 {
|
if len(errorsByBackend) == 0 {
|
||||||
@@ -142,51 +159,58 @@ func d3d11H264FailureKeyFor(backend string, req h264D3D11TextureRequest) d3d11H2
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func activeD3D11H264Failure(key d3d11H264FailureKey, now time.Time) (d3d11H264Failure, bool) {
|
func (r *h264D3D11TextureRegistry) activeFailure(key d3d11H264FailureKey, now time.Time) (d3d11H264Failure, bool) {
|
||||||
failure, ok := d3d11H264TextureRegistry.failures[key]
|
failure, ok := r.failures[key]
|
||||||
if !ok {
|
if !ok {
|
||||||
return d3d11H264Failure{}, false
|
return d3d11H264Failure{}, false
|
||||||
}
|
}
|
||||||
if !now.Before(failure.until) {
|
if !now.Before(failure.until) {
|
||||||
delete(d3d11H264TextureRegistry.failures, key)
|
delete(r.failures, key)
|
||||||
return d3d11H264Failure{}, false
|
return d3d11H264Failure{}, false
|
||||||
}
|
}
|
||||||
return failure, true
|
return failure, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func rememberD3D11H264Failure(key d3d11H264FailureKey, err error) {
|
func (r *h264D3D11TextureRegistry) rememberFailure(key d3d11H264FailureKey, err error) {
|
||||||
if len(d3d11H264TextureRegistry.failures) > 128 {
|
if len(r.failures) > 128 {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
for existingKey, failure := range d3d11H264TextureRegistry.failures {
|
for existingKey, failure := range r.failures {
|
||||||
if !now.Before(failure.until) {
|
if !now.Before(failure.until) {
|
||||||
delete(d3d11H264TextureRegistry.failures, existingKey)
|
delete(r.failures, existingKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
d3d11H264TextureRegistry.failures[key] = d3d11H264Failure{until: time.Now().Add(d3d11H264FailureCooldown), err: err.Error()}
|
r.failures[key] = d3d11H264Failure{until: time.Now().Add(d3d11H264FailureCooldown), err: err.Error()}
|
||||||
}
|
}
|
||||||
|
|
||||||
func requestH264D3D11TextureKeyframe() {
|
func requestH264D3D11TextureKeyframe(stream string) {
|
||||||
d3d11H264TextureRegistry.Lock()
|
registry := h264D3D11Registry(stream)
|
||||||
defer d3d11H264TextureRegistry.Unlock()
|
registry.Lock()
|
||||||
if d3d11H264TextureRegistry.active != nil {
|
defer registry.Unlock()
|
||||||
d3d11H264TextureRegistry.active.RequestKeyframe()
|
if registry.active != nil {
|
||||||
|
registry.active.RequestKeyframe()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, backend := range d3d11H264TextureRegistry.backends {
|
for _, backend := range registry.backends {
|
||||||
if backend != nil {
|
if backend != nil {
|
||||||
backend.RequestKeyframe()
|
backend.RequestKeyframe()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func resetH264D3D11TextureEncoder() {
|
func resetH264D3D11TextureEncoder(stream string) {
|
||||||
d3d11H264TextureRegistry.Lock()
|
registry := h264D3D11Registry(stream)
|
||||||
defer d3d11H264TextureRegistry.Unlock()
|
registry.Lock()
|
||||||
for _, backend := range d3d11H264TextureRegistry.backends {
|
defer registry.Unlock()
|
||||||
|
for _, backend := range registry.backends {
|
||||||
if backend != nil {
|
if backend != nil {
|
||||||
backend.Reset()
|
backend.Reset()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
d3d11H264TextureRegistry.active = nil
|
registry.active = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetAllH264D3D11TextureEncoders() {
|
||||||
|
resetH264D3D11TextureEncoder("desktop")
|
||||||
|
resetH264D3D11TextureEncoder("backstage")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ package capture
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"image"
|
||||||
"testing"
|
"testing"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
)
|
)
|
||||||
@@ -54,7 +55,7 @@ func TestD3D11TextureBackendSelectionAndFailover(t *testing.T) {
|
|||||||
|
|
||||||
device, texture := 1, 2
|
device, texture := 1, 2
|
||||||
req := h264D3D11TextureRequest{Device: unsafe.Pointer(&device), Texture: unsafe.Pointer(&texture)}
|
req := h264D3D11TextureRequest{Device: unsafe.Pointer(&device), Texture: unsafe.Pointer(&texture)}
|
||||||
out, provider, err := encodeH264D3D11Texture(req)
|
out, provider, err := encodeH264D3D11Texture("desktop", req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("first encode: %v", err)
|
t.Fatalf("first encode: %v", err)
|
||||||
}
|
}
|
||||||
@@ -65,7 +66,7 @@ func TestD3D11TextureBackendSelectionAndFailover(t *testing.T) {
|
|||||||
t.Fatalf("unexpected calls first.reset=%d second.encode=%d", first.resetCalls, second.encodeCalls)
|
t.Fatalf("unexpected calls first.reset=%d second.encode=%d", first.resetCalls, second.encodeCalls)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, provider, err = encodeH264D3D11Texture(req)
|
_, provider, err = encodeH264D3D11Texture("desktop", req)
|
||||||
if err != nil || provider != "second" {
|
if err != nil || provider != "second" {
|
||||||
t.Fatalf("active backend reuse provider=%q err=%v", provider, err)
|
t.Fatalf("active backend reuse provider=%q err=%v", provider, err)
|
||||||
}
|
}
|
||||||
@@ -73,11 +74,11 @@ func TestD3D11TextureBackendSelectionAndFailover(t *testing.T) {
|
|||||||
t.Fatalf("active backend was not reused: first=%d second=%d", first.encodeCalls, second.encodeCalls)
|
t.Fatalf("active backend was not reused: first=%d second=%d", first.encodeCalls, second.encodeCalls)
|
||||||
}
|
}
|
||||||
|
|
||||||
requestH264D3D11TextureKeyframe()
|
requestH264D3D11TextureKeyframe("desktop")
|
||||||
if second.keyframeCalls != 1 || first.keyframeCalls != 0 {
|
if second.keyframeCalls != 1 || first.keyframeCalls != 0 {
|
||||||
t.Fatalf("keyframe routed incorrectly: first=%d second=%d", first.keyframeCalls, second.keyframeCalls)
|
t.Fatalf("keyframe routed incorrectly: first=%d second=%d", first.keyframeCalls, second.keyframeCalls)
|
||||||
}
|
}
|
||||||
resetH264D3D11TextureEncoder()
|
resetH264D3D11TextureEncoder("desktop")
|
||||||
if first.resetCalls != 2 || second.resetCalls != 1 {
|
if first.resetCalls != 2 || second.resetCalls != 1 {
|
||||||
t.Fatalf("reset did not reach every backend: first=%d second=%d", first.resetCalls, second.resetCalls)
|
t.Fatalf("reset did not reach every backend: first=%d second=%d", first.resetCalls, second.resetCalls)
|
||||||
}
|
}
|
||||||
@@ -105,13 +106,129 @@ func TestD3D11TextureBackendFailuresUseCooldown(t *testing.T) {
|
|||||||
|
|
||||||
device, texture := 1, 2
|
device, texture := 1, 2
|
||||||
req := h264D3D11TextureRequest{Device: unsafe.Pointer(&device), Texture: unsafe.Pointer(&texture), EncodeWidth: 2560, EncodeHeight: 1440, FPS: 240}
|
req := h264D3D11TextureRequest{Device: unsafe.Pointer(&device), Texture: unsafe.Pointer(&texture), EncodeWidth: 2560, EncodeHeight: 1440, FPS: 240}
|
||||||
if _, _, err := encodeH264D3D11Texture(req); err == nil {
|
if _, _, err := encodeH264D3D11Texture("desktop", req); err == nil {
|
||||||
t.Fatal("expected initial profile failure")
|
t.Fatal("expected initial profile failure")
|
||||||
}
|
}
|
||||||
if _, _, err := encodeH264D3D11Texture(req); err == nil {
|
if _, _, err := encodeH264D3D11Texture("desktop", req); err == nil {
|
||||||
t.Fatal("expected cached profile failure")
|
t.Fatal("expected cached profile failure")
|
||||||
}
|
}
|
||||||
if first.encodeCalls != 1 || second.encodeCalls != 1 {
|
if first.encodeCalls != 1 || second.encodeCalls != 1 {
|
||||||
t.Fatalf("failed profile was retried during cooldown: first=%d second=%d", first.encodeCalls, second.encodeCalls)
|
t.Fatalf("failed profile was retried during cooldown: first=%d second=%d", first.encodeCalls, second.encodeCalls)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type fakeH264FrameEncoder struct {
|
||||||
|
output []byte
|
||||||
|
err error
|
||||||
|
encodeCall int
|
||||||
|
closed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *fakeH264FrameEncoder) Encode(*image.RGBA) ([]byte, error) {
|
||||||
|
e.encodeCall++
|
||||||
|
return e.output, e.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *fakeH264FrameEncoder) Close() {
|
||||||
|
e.closed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *fakeH264FrameEncoder) Matches(_, _, _ int) bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestH264RuntimeFailureFallsBackPerStream(t *testing.T) {
|
||||||
|
primary := &fakeH264FrameEncoder{err: errors.New("NVENC session failed")}
|
||||||
|
software := &fakeH264FrameEncoder{output: []byte{7, 8, 9}}
|
||||||
|
var slot h264FrameEncoder = primary
|
||||||
|
factoryCalls := 0
|
||||||
|
factory := func(width, height, fps int) (h264FrameEncoder, error) {
|
||||||
|
factoryCalls++
|
||||||
|
if width != 64 || height != 32 || fps != 47 {
|
||||||
|
t.Fatalf("fallback config = %dx%d@%d, want 64x32@47", width, height, fps)
|
||||||
|
}
|
||||||
|
return software, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := encodeH264FrameWithRuntimeFallback(
|
||||||
|
&slot,
|
||||||
|
"backstage",
|
||||||
|
image.NewRGBA(image.Rect(0, 0, 64, 32)),
|
||||||
|
47,
|
||||||
|
factory,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("runtime fallback failed: %v", err)
|
||||||
|
}
|
||||||
|
if factoryCalls != 1 || primary.encodeCall != 1 || !primary.closed {
|
||||||
|
t.Fatalf("primary cleanup/fallback calls = factory:%d encode:%d closed:%v", factoryCalls, primary.encodeCall, primary.closed)
|
||||||
|
}
|
||||||
|
if slot != software || software.encodeCall != 1 {
|
||||||
|
t.Fatalf("software encoder was not retained after fallback")
|
||||||
|
}
|
||||||
|
if len(out) != 3 || out[0] != 7 {
|
||||||
|
t.Fatalf("fallback output = %v", out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestH264TargetFPSIsIndependentPerStream(t *testing.T) {
|
||||||
|
SetDesktopH264TargetFPS(120)
|
||||||
|
SetBackstageH264TargetFPS(37)
|
||||||
|
SetWebcamH264TargetFPS(24)
|
||||||
|
|
||||||
|
if got := activeH264FPS(); got != 120 {
|
||||||
|
t.Fatalf("desktop fps = %d, want 120", got)
|
||||||
|
}
|
||||||
|
if got := activeH264FPSForStream("backstage"); got != 37 {
|
||||||
|
t.Fatalf("backstage fps = %d, want 37", got)
|
||||||
|
}
|
||||||
|
if got := activeH264FPSForStream("webcam"); got != 24 {
|
||||||
|
t.Fatalf("webcam fps = %d, want 24", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestD3D11TextureEncodersAreIsolatedByStream(t *testing.T) {
|
||||||
|
desktopBackend := &fakeD3D11TextureBackend{name: "desktop", outputs: [][]byte{{1}, {2}}}
|
||||||
|
backstageBackend := &fakeD3D11TextureBackend{name: "backstage", outputs: [][]byte{{3}}}
|
||||||
|
savedDesktop := d3d11H264TextureRegistry
|
||||||
|
savedBackstage := backstageD3D11H264TextureRegistry
|
||||||
|
d3d11H264TextureRegistry = &h264D3D11TextureRegistry{
|
||||||
|
backends: []h264D3D11TextureBackend{desktopBackend},
|
||||||
|
failures: make(map[d3d11H264FailureKey]d3d11H264Failure),
|
||||||
|
}
|
||||||
|
backstageD3D11H264TextureRegistry = &h264D3D11TextureRegistry{
|
||||||
|
backends: []h264D3D11TextureBackend{backstageBackend},
|
||||||
|
failures: make(map[d3d11H264FailureKey]d3d11H264Failure),
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
d3d11H264TextureRegistry = savedDesktop
|
||||||
|
backstageD3D11H264TextureRegistry = savedBackstage
|
||||||
|
})
|
||||||
|
|
||||||
|
desktopDevice, desktopTexture := 1, 2
|
||||||
|
backstageDevice, backstageTexture := 3, 4
|
||||||
|
desktopReq := h264D3D11TextureRequest{
|
||||||
|
Device: unsafe.Pointer(&desktopDevice), Texture: unsafe.Pointer(&desktopTexture),
|
||||||
|
EncodeWidth: 1920, EncodeHeight: 1080, FPS: 60,
|
||||||
|
}
|
||||||
|
backstageReq := h264D3D11TextureRequest{
|
||||||
|
Device: unsafe.Pointer(&backstageDevice), Texture: unsafe.Pointer(&backstageTexture),
|
||||||
|
EncodeWidth: 1280, EncodeHeight: 720, FPS: 30,
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, _, err := encodeH264D3D11Texture("desktop", desktopReq); err != nil {
|
||||||
|
t.Fatalf("desktop encode: %v", err)
|
||||||
|
}
|
||||||
|
if _, _, err := encodeH264D3D11Texture("backstage", backstageReq); err != nil {
|
||||||
|
t.Fatalf("backstage encode: %v", err)
|
||||||
|
}
|
||||||
|
if _, _, err := encodeH264D3D11Texture("desktop", desktopReq); err != nil {
|
||||||
|
t.Fatalf("second desktop encode: %v", err)
|
||||||
|
}
|
||||||
|
if desktopBackend.encodeCalls != 2 || backstageBackend.encodeCalls != 1 {
|
||||||
|
t.Fatalf("stream calls crossed: desktop=%d backstage=%d", desktopBackend.encodeCalls, backstageBackend.encodeCalls)
|
||||||
|
}
|
||||||
|
if desktopBackend.resetCalls != 0 || backstageBackend.resetCalls != 0 {
|
||||||
|
t.Fatalf("one stream reset the other: desktop=%d backstage=%d", desktopBackend.resetCalls, backstageBackend.resetCalls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,13 +17,13 @@ func nativeH264AvailabilityDetail() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func encodeNativeH264D3D11Texture(device, texture unsafe.Pointer, inputWidth, inputHeight, encodeWidth, encodeHeight, fps int, dxgiFormat uint32, forceIDR bool) ([]byte, error) {
|
func encodeNativeH264D3D11Texture(_ string, device, texture unsafe.Pointer, inputWidth, inputHeight, encodeWidth, encodeHeight, fps int, dxgiFormat uint32, forceIDR bool) ([]byte, error) {
|
||||||
return nil, fmt.Errorf("native NVENC D3D11 texture encode requires cgo for input=%dx%d output=%dx%d@%dfps", inputWidth, inputHeight, encodeWidth, encodeHeight, fps)
|
return nil, fmt.Errorf("native NVENC D3D11 texture encode requires cgo for input=%dx%d output=%dx%d@%dfps", inputWidth, inputHeight, encodeWidth, encodeHeight, fps)
|
||||||
}
|
}
|
||||||
|
|
||||||
func requestNativeH264D3D11TextureKeyframe() {}
|
func requestNativeH264D3D11TextureKeyframe(_ string) {}
|
||||||
|
|
||||||
func resetNativeH264D3D11TextureEncoder() {}
|
func resetNativeH264D3D11TextureEncoder(_ string) {}
|
||||||
|
|
||||||
func encodeNativeHEVCD3D11Texture(device, texture unsafe.Pointer, inputWidth, inputHeight, encodeWidth, encodeHeight, fps int, dxgiFormat uint32, forceIDR bool) ([]byte, error) {
|
func encodeNativeHEVCD3D11Texture(device, texture unsafe.Pointer, inputWidth, inputHeight, encodeWidth, encodeHeight, fps int, dxgiFormat uint32, forceIDR bool) ([]byte, error) {
|
||||||
return nil, fmt.Errorf("native NVENC HEVC D3D11 texture encode requires cgo for input=%dx%d output=%dx%d@%dfps", inputWidth, inputHeight, encodeWidth, encodeHeight, fps)
|
return nil, fmt.Errorf("native NVENC HEVC D3D11 texture encode requires cgo for input=%dx%d output=%dx%d@%dfps", inputWidth, inputHeight, encodeWidth, encodeHeight, fps)
|
||||||
|
|||||||
@@ -1921,48 +1921,62 @@ func envBool(name string) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type nativeTextureH264StreamState struct {
|
||||||
|
sync.Mutex
|
||||||
|
encoder *nativeD3D11TextureH264Encoder
|
||||||
|
forceIDR atomic.Bool
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
nativeTextureH264Mu sync.Mutex
|
nativeTextureH264Streams sync.Map
|
||||||
nativeTextureH264Enc *nativeD3D11TextureH264Encoder
|
|
||||||
nativeTextureH264ForceIDR atomic.Bool
|
|
||||||
nativeTextureHEVCMu sync.Mutex
|
nativeTextureHEVCMu sync.Mutex
|
||||||
nativeTextureHEVCEnc *nativeD3D11TextureH264Encoder
|
nativeTextureHEVCEnc *nativeD3D11TextureH264Encoder
|
||||||
nativeTextureHEVCForceIDR atomic.Bool
|
nativeTextureHEVCForceIDR atomic.Bool
|
||||||
)
|
)
|
||||||
|
|
||||||
func encodeNativeH264D3D11Texture(device, texture unsafe.Pointer, inputWidth, inputHeight, encodeWidth, encodeHeight, fps int, dxgiFormat uint32, forceIDR bool) ([]byte, error) {
|
func nativeTextureH264State(stream string) *nativeTextureH264StreamState {
|
||||||
|
if stream == "" {
|
||||||
|
stream = "desktop"
|
||||||
|
}
|
||||||
|
state, _ := nativeTextureH264Streams.LoadOrStore(stream, &nativeTextureH264StreamState{})
|
||||||
|
return state.(*nativeTextureH264StreamState)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeNativeH264D3D11Texture(stream string, device, texture unsafe.Pointer, inputWidth, inputHeight, encodeWidth, encodeHeight, fps int, dxgiFormat uint32, forceIDR bool) ([]byte, error) {
|
||||||
if device == nil || texture == nil {
|
if device == nil || texture == nil {
|
||||||
return nil, fmt.Errorf("nil D3D11 device or texture")
|
return nil, fmt.Errorf("nil D3D11 device or texture")
|
||||||
}
|
}
|
||||||
nativeTextureH264Mu.Lock()
|
state := nativeTextureH264State(stream)
|
||||||
defer nativeTextureH264Mu.Unlock()
|
state.Lock()
|
||||||
|
defer state.Unlock()
|
||||||
|
|
||||||
if nativeTextureH264Enc == nil || !nativeTextureH264Enc.Matches(device, inputWidth, inputHeight, encodeWidth, encodeHeight, fps, dxgiFormat) {
|
if state.encoder == nil || !state.encoder.Matches(device, inputWidth, inputHeight, encodeWidth, encodeHeight, fps, dxgiFormat) {
|
||||||
if nativeTextureH264Enc != nil {
|
if state.encoder != nil {
|
||||||
nativeTextureH264Enc.Close()
|
state.encoder.Close()
|
||||||
nativeTextureH264Enc = nil
|
state.encoder = nil
|
||||||
}
|
}
|
||||||
enc, err := newNativeD3D11TextureH264Encoder(device, inputWidth, inputHeight, encodeWidth, encodeHeight, fps, dxgiFormat)
|
enc, err := newNativeD3D11TextureH264Encoder(device, inputWidth, inputHeight, encodeWidth, encodeHeight, fps, dxgiFormat)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
nativeTextureH264Enc = enc
|
state.encoder = enc
|
||||||
}
|
}
|
||||||
return nativeTextureH264Enc.EncodeTexture(texture, forceIDR || nativeTextureH264ForceIDR.Swap(false))
|
return state.encoder.EncodeTexture(texture, forceIDR || state.forceIDR.Swap(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
func requestNativeH264D3D11TextureKeyframe() {
|
func requestNativeH264D3D11TextureKeyframe(stream string) {
|
||||||
nativeTextureH264ForceIDR.Store(true)
|
nativeTextureH264State(stream).forceIDR.Store(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func resetNativeH264D3D11TextureEncoder() {
|
func resetNativeH264D3D11TextureEncoder(stream string) {
|
||||||
nativeTextureH264Mu.Lock()
|
state := nativeTextureH264State(stream)
|
||||||
defer nativeTextureH264Mu.Unlock()
|
state.Lock()
|
||||||
if nativeTextureH264Enc != nil {
|
defer state.Unlock()
|
||||||
nativeTextureH264Enc.Close()
|
if state.encoder != nil {
|
||||||
nativeTextureH264Enc = nil
|
state.encoder.Close()
|
||||||
|
state.encoder = nil
|
||||||
}
|
}
|
||||||
nativeTextureH264ForceIDR.Store(false)
|
state.forceIDR.Store(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func encodeNativeHEVCD3D11Texture(device, texture unsafe.Pointer, inputWidth, inputHeight, encodeWidth, encodeHeight, fps int, dxgiFormat uint32, forceIDR bool) ([]byte, error) {
|
func encodeNativeHEVCD3D11Texture(device, texture unsafe.Pointer, inputWidth, inputHeight, encodeWidth, encodeHeight, fps int, dxgiFormat uint32, forceIDR bool) ([]byte, error) {
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func VirtualTryDirectH264Frame() (wire.Frame, time.Duration, time.Duration, bool, error) {
|
func VirtualTryDirectH264Frame() (wire.Frame, time.Duration, time.Duration, bool, error) {
|
||||||
if blockCodec() != "h264" {
|
if backstageCodec() != "h264" {
|
||||||
return wire.Frame{}, 0, 0, false, nil
|
return wire.Frame{}, 0, 0, false, nil
|
||||||
}
|
}
|
||||||
return virtualDupState.captureDirectH264()
|
return virtualDupState.captureDirectH264()
|
||||||
@@ -280,12 +280,12 @@ func (s *virtualDuplicationState) captureDirectH264() (wire.Frame, time.Duration
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *virtualDuplicationState) encodeCachedDirectH264(capStart time.Time, width, height int) (wire.Frame, time.Duration, time.Duration, bool, error) {
|
func (s *virtualDuplicationState) encodeCachedDirectH264(capStart time.Time, width, height int) (wire.Frame, time.Duration, time.Duration, bool, error) {
|
||||||
fps := activeH264FPS()
|
fps := activeH264FPSForStream("backstage")
|
||||||
encStart := time.Now()
|
encStart := time.Now()
|
||||||
out, _, err := encodeH264D3D11Texture(h264D3D11TextureRequest{
|
out, _, err := encodeH264D3D11Texture("backstage", h264D3D11TextureRequest{
|
||||||
Device: unsafe.Pointer(s.device), Texture: unsafe.Pointer(s.h264LastTex),
|
Device: unsafe.Pointer(s.device), Texture: unsafe.Pointer(s.h264LastTex),
|
||||||
InputWidth: width, InputHeight: height, EncodeWidth: width, EncodeHeight: height,
|
InputWidth: width, InputHeight: height, EncodeWidth: width, EncodeHeight: height,
|
||||||
FPS: fps, DXGIFormat: s.h264LastDesc.Format, ForceIDR: webrtcpub.ConsumeKeyframeRequest(),
|
FPS: fps, DXGIFormat: s.h264LastDesc.Format, ForceIDR: webrtcpub.ConsumeKeyframeRequest(webrtcpub.Kindbackstage),
|
||||||
})
|
})
|
||||||
encodeDur := time.Since(encStart)
|
encodeDur := time.Since(encStart)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -688,7 +688,7 @@ func (s *duplicationState) closeLocked() {
|
|||||||
s.h264LastTex = nil
|
s.h264LastTex = nil
|
||||||
s.h264LastDesc = d3d11Texture2DDesc{}
|
s.h264LastDesc = d3d11Texture2DDesc{}
|
||||||
}
|
}
|
||||||
resetH264D3D11TextureEncoder()
|
resetH264D3D11TextureEncoder("desktop")
|
||||||
resetNativeHEVCD3D11TextureEncoder()
|
resetNativeHEVCD3D11TextureEncoder()
|
||||||
if s.context != nil {
|
if s.context != nil {
|
||||||
s.context.Release()
|
s.context.Release()
|
||||||
@@ -1027,7 +1027,7 @@ func encodeDesktopD3D11Texture(codec string, req h264D3D11TextureRequest) ([]byt
|
|||||||
req.EncodeWidth, req.EncodeHeight, req.FPS, req.DXGIFormat, req.ForceIDR)
|
req.EncodeWidth, req.EncodeHeight, req.FPS, req.DXGIFormat, req.ForceIDR)
|
||||||
return out, "NVIDIA NVENC", err
|
return out, "NVIDIA NVENC", err
|
||||||
}
|
}
|
||||||
return encodeH264D3D11Texture(req)
|
return encodeH264D3D11Texture("desktop", req)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *duplicationState) cacheH264TextureLocked(src *d3d11Texture2D, srcDesc d3d11Texture2DDesc) error {
|
func (s *duplicationState) cacheH264TextureLocked(src *d3d11Texture2D, srcDesc d3d11Texture2DDesc) error {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ func backstageStart(ctx context.Context, env *rt.Env, autoStartExplorer bool) er
|
|||||||
//garble:controlflow block_splits=10 junk_jumps=10 flatten_passes=2
|
//garble:controlflow block_splits=10 junk_jumps=10 flatten_passes=2
|
||||||
fps := activebackstageTargetFPS()
|
fps := activebackstageTargetFPS()
|
||||||
interval := time.Second / time.Duration(fps)
|
interval := time.Second / time.Duration(fps)
|
||||||
capture.SetH264TargetFPS(fps)
|
capture.SetBackstageH264TargetFPS(fps)
|
||||||
capture.SetFrameFlowTargetFPS(fps)
|
capture.SetFrameFlowTargetFPS(fps)
|
||||||
log.Printf("backstage: starting stream (target fps %d)", fps)
|
log.Printf("backstage: starting stream (target fps %d)", fps)
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ func backstageStart(ctx context.Context, env *rt.Env, autoStartExplorer bool) er
|
|||||||
fps := activebackstageTargetFPS()
|
fps := activebackstageTargetFPS()
|
||||||
if fps != currentFPS {
|
if fps != currentFPS {
|
||||||
currentFPS = fps
|
currentFPS = fps
|
||||||
capture.SetH264TargetFPS(fps)
|
capture.SetBackstageH264TargetFPS(fps)
|
||||||
capture.SetFrameFlowTargetFPS(fps)
|
capture.SetFrameFlowTargetFPS(fps)
|
||||||
ticker.Reset(time.Second / time.Duration(fps))
|
ticker.Reset(time.Second / time.Duration(fps))
|
||||||
log.Printf("backstage: target fps changed to %d", fps)
|
log.Printf("backstage: target fps changed to %d", fps)
|
||||||
@@ -90,7 +90,7 @@ func activebackstageTargetFPS() int {
|
|||||||
func SetbackstageTargetFPS(fps int) int {
|
func SetbackstageTargetFPS(fps int) int {
|
||||||
fps = clampDesktopTargetFPS(fps)
|
fps = clampDesktopTargetFPS(fps)
|
||||||
backstageTargetFPS.Store(int64(fps))
|
backstageTargetFPS.Store(int64(fps))
|
||||||
capture.SetH264TargetFPS(fps)
|
capture.SetBackstageH264TargetFPS(fps)
|
||||||
capture.SetFrameFlowTargetFPS(fps)
|
capture.SetFrameFlowTargetFPS(fps)
|
||||||
return fps
|
return fps
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -832,7 +832,7 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
|
|||||||
log.Printf("desktop: set quality=%d codec=%s software_h264=%v", quality, codec, softwareH264)
|
log.Printf("desktop: set quality=%d codec=%s software_h264=%v", quality, codec, softwareH264)
|
||||||
}
|
}
|
||||||
capture.SetDesktopSoftwareH264(softwareH264)
|
capture.SetDesktopSoftwareH264(softwareH264)
|
||||||
capture.SetQualityAndCodec(quality, codec)
|
capture.SetDesktopQualityAndCodec(quality, codec)
|
||||||
sendCommandResultSafe(env, cmdID, true, "")
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
return nil
|
return nil
|
||||||
case "desktop_request_keyframe":
|
case "desktop_request_keyframe":
|
||||||
@@ -1335,7 +1335,7 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.Printf("backstage: set quality=%d codec=%s", quality, codec)
|
log.Printf("backstage: set quality=%d codec=%s", quality, codec)
|
||||||
capture.SetQualityAndCodec(quality, codec)
|
capture.SetBackstageQualityAndCodec(quality, codec)
|
||||||
sendCommandResultSafe(env, cmdID, true, "")
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
return nil
|
return nil
|
||||||
case "backstage_request_keyframe":
|
case "backstage_request_keyframe":
|
||||||
@@ -2209,7 +2209,7 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.Printf("hidden: set quality=%d codec=%s", quality, codec)
|
log.Printf("hidden: set quality=%d codec=%s", quality, codec)
|
||||||
capture.SetQualityAndCodec(quality, codec)
|
capture.SetBackstageQualityAndCodec(quality, codec)
|
||||||
sendCommandResultSafe(env, cmdID, true, "")
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
return nil
|
return nil
|
||||||
case "virtual_request_keyframe":
|
case "virtual_request_keyframe":
|
||||||
@@ -2447,6 +2447,7 @@ func HandleCommand(ctx context.Context, env *runtime.Env, envelope map[string]in
|
|||||||
fps = clampedFPS
|
fps = clampedFPS
|
||||||
}
|
}
|
||||||
env.WebcamFPS = fps
|
env.WebcamFPS = fps
|
||||||
|
capture.SetWebcamH264TargetFPS(fps)
|
||||||
env.WebcamUseMaxFPS = useMax
|
env.WebcamUseMaxFPS = useMax
|
||||||
sendCommandResultSafe(env, cmdID, true, "")
|
sendCommandResultSafe(env, cmdID, true, "")
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ func DesktopStart(ctx context.Context, env *rt.Env) error {
|
|||||||
//garble:controlflow block_splits=10 junk_jumps=10 flatten_passes=2
|
//garble:controlflow block_splits=10 junk_jumps=10 flatten_passes=2
|
||||||
fps := activeDesktopTargetFPS()
|
fps := activeDesktopTargetFPS()
|
||||||
interval := time.Second / time.Duration(fps)
|
interval := time.Second / time.Duration(fps)
|
||||||
capture.SetH264TargetFPS(fps)
|
capture.SetDesktopH264TargetFPS(fps)
|
||||||
capture.SetFrameFlowTargetFPS(fps)
|
capture.SetFrameFlowTargetFPS(fps)
|
||||||
log.Printf("desktop: starting stream (target fps %d)", fps)
|
log.Printf("desktop: starting stream (target fps %d)", fps)
|
||||||
ticker := time.NewTicker(interval)
|
ticker := time.NewTicker(interval)
|
||||||
@@ -47,7 +47,7 @@ func DesktopStart(ctx context.Context, env *rt.Env) error {
|
|||||||
fps := activeDesktopTargetFPS()
|
fps := activeDesktopTargetFPS()
|
||||||
if fps != currentFPS {
|
if fps != currentFPS {
|
||||||
currentFPS = fps
|
currentFPS = fps
|
||||||
capture.SetH264TargetFPS(fps)
|
capture.SetDesktopH264TargetFPS(fps)
|
||||||
capture.SetFrameFlowTargetFPS(fps)
|
capture.SetFrameFlowTargetFPS(fps)
|
||||||
ticker.Reset(time.Second / time.Duration(fps))
|
ticker.Reset(time.Second / time.Duration(fps))
|
||||||
log.Printf("desktop: target fps changed to %d", fps)
|
log.Printf("desktop: target fps changed to %d", fps)
|
||||||
@@ -74,7 +74,7 @@ func activeDesktopTargetFPS() int {
|
|||||||
func SetDesktopTargetFPS(fps int) int {
|
func SetDesktopTargetFPS(fps int) int {
|
||||||
fps = clampDesktopTargetFPS(fps)
|
fps = clampDesktopTargetFPS(fps)
|
||||||
desktopTargetFPS.Store(int64(fps))
|
desktopTargetFPS.Store(int64(fps))
|
||||||
capture.SetH264TargetFPS(fps)
|
capture.SetDesktopH264TargetFPS(fps)
|
||||||
capture.SetFrameFlowTargetFPS(fps)
|
capture.SetFrameFlowTargetFPS(fps)
|
||||||
return fps
|
return fps
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ var (
|
|||||||
func VirtualStart(ctx context.Context, env *rt.Env) error {
|
func VirtualStart(ctx context.Context, env *rt.Env) error {
|
||||||
fps := activeVirtualTargetFPS()
|
fps := activeVirtualTargetFPS()
|
||||||
interval := time.Second / time.Duration(fps)
|
interval := time.Second / time.Duration(fps)
|
||||||
capture.SetH264TargetFPS(fps)
|
capture.SetBackstageH264TargetFPS(fps)
|
||||||
capture.SetFrameFlowTargetFPS(fps)
|
capture.SetFrameFlowTargetFPS(fps)
|
||||||
log.Printf("virtual: starting stream (target fps %d)", fps)
|
log.Printf("virtual: starting stream (target fps %d)", fps)
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ func VirtualStart(ctx context.Context, env *rt.Env) error {
|
|||||||
fps := activeVirtualTargetFPS()
|
fps := activeVirtualTargetFPS()
|
||||||
if fps != currentFPS {
|
if fps != currentFPS {
|
||||||
currentFPS = fps
|
currentFPS = fps
|
||||||
capture.SetH264TargetFPS(fps)
|
capture.SetBackstageH264TargetFPS(fps)
|
||||||
capture.SetFrameFlowTargetFPS(fps)
|
capture.SetFrameFlowTargetFPS(fps)
|
||||||
ticker.Reset(time.Second / time.Duration(fps))
|
ticker.Reset(time.Second / time.Duration(fps))
|
||||||
log.Printf("virtual: target fps changed to %d", fps)
|
log.Printf("virtual: target fps changed to %d", fps)
|
||||||
@@ -65,7 +65,7 @@ func activeVirtualTargetFPS() int {
|
|||||||
func SetVirtualTargetFPS(fps int) int {
|
func SetVirtualTargetFPS(fps int) int {
|
||||||
fps = clampDesktopTargetFPS(fps)
|
fps = clampDesktopTargetFPS(fps)
|
||||||
virtualTargetFPS.Store(int64(fps))
|
virtualTargetFPS.Store(int64(fps))
|
||||||
capture.SetH264TargetFPS(fps)
|
capture.SetBackstageH264TargetFPS(fps)
|
||||||
capture.SetFrameFlowTargetFPS(fps)
|
capture.SetFrameFlowTargetFPS(fps)
|
||||||
return fps
|
return fps
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ func WebcamStart(ctx context.Context, env *rt.Env) error {
|
|||||||
if fps > 120 {
|
if fps > 120 {
|
||||||
fps = 120
|
fps = 120
|
||||||
}
|
}
|
||||||
|
capture.SetWebcamH264TargetFPS(fps)
|
||||||
interval := time.Second / time.Duration(fps)
|
interval := time.Second / time.Duration(fps)
|
||||||
log.Printf("webcam: starting stream (max fps %d)", fps)
|
log.Printf("webcam: starting stream (max fps %d)", fps)
|
||||||
defer capture.CleanupWebcam()
|
defer capture.CleanupWebcam()
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ func StartP2POffer(ctx context.Context, kind Kind, sessionID string, offerSDP st
|
|||||||
return "", fmt.Errorf("add video transceiver: %w", err)
|
return "", fmt.Errorf("add video transceiver: %w", err)
|
||||||
}
|
}
|
||||||
if sender := tx.Sender(); sender != nil {
|
if sender := tx.Sender(); sender != nil {
|
||||||
go drainRTCP(sender)
|
go drainRTCP(sender, kind)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if hasAudio {
|
if hasAudio {
|
||||||
@@ -172,7 +172,7 @@ func StartP2POffer(ctx context.Context, kind Kind, sessionID string, offerSDP st
|
|||||||
return "", fmt.Errorf("add audio transceiver: %w", err)
|
return "", fmt.Errorf("add audio transceiver: %w", err)
|
||||||
}
|
}
|
||||||
if sender := tx.Sender(); sender != nil {
|
if sender := tx.Sender(); sender != nil {
|
||||||
go drainRTCP(sender)
|
go drainRTCP(sender, "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ func registerVideoWriter(kind Kind, id string, w VideoWriter) {
|
|||||||
entry.video = newLatestVideoWriter(w)
|
entry.video = newLatestVideoWriter(w)
|
||||||
bucket[id] = entry
|
bucket[id] = entry
|
||||||
writersMu.Unlock()
|
writersMu.Unlock()
|
||||||
RequestKeyframe()
|
RequestKeyframe(kind)
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerAudioWriter(kind Kind, id string, w AudioWriter) {
|
func registerAudioWriter(kind Kind, id string, w AudioWriter) {
|
||||||
@@ -155,14 +155,29 @@ func IsActive(kind Kind) bool {
|
|||||||
return len(writers[string(kind)]) > 0
|
return len(writers[string(kind)]) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
var keyframeWanted atomic.Bool
|
var (
|
||||||
|
desktopKeyframeWanted atomic.Bool
|
||||||
|
backstageKeyframeWanted atomic.Bool
|
||||||
|
webcamKeyframeWanted atomic.Bool
|
||||||
|
)
|
||||||
|
|
||||||
func RequestKeyframe() {
|
func keyframeRequestFlag(kind Kind) *atomic.Bool {
|
||||||
keyframeWanted.Store(true)
|
switch kind {
|
||||||
|
case Kindbackstage:
|
||||||
|
return &backstageKeyframeWanted
|
||||||
|
case KindWebcam:
|
||||||
|
return &webcamKeyframeWanted
|
||||||
|
default:
|
||||||
|
return &desktopKeyframeWanted
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ConsumeKeyframeRequest() bool {
|
func RequestKeyframe(kind Kind) {
|
||||||
return keyframeWanted.Swap(false)
|
keyframeRequestFlag(kind).Store(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ConsumeKeyframeRequest(kind Kind) bool {
|
||||||
|
return keyframeRequestFlag(kind).Swap(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func WriteH264(kind Kind, nalu []byte, dur time.Duration) error {
|
func WriteH264(kind Kind, nalu []byte, dur time.Duration) error {
|
||||||
|
|||||||
@@ -67,3 +67,21 @@ func TestWriteH264DoesNotBlockAndKeepsLatestPendingFrame(t *testing.T) {
|
|||||||
}
|
}
|
||||||
t.Fatal("latest pending frame was not delivered")
|
t.Fatal("latest pending frame was not delivered")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestKeyframeRequestsAreIsolatedByStreamKind(t *testing.T) {
|
||||||
|
for _, kind := range []Kind{KindDesktop, Kindbackstage, KindWebcam} {
|
||||||
|
_ = ConsumeKeyframeRequest(kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
RequestKeyframe(Kindbackstage)
|
||||||
|
|
||||||
|
if ConsumeKeyframeRequest(KindDesktop) {
|
||||||
|
t.Fatal("backstage keyframe request leaked into desktop stream")
|
||||||
|
}
|
||||||
|
if ConsumeKeyframeRequest(KindWebcam) {
|
||||||
|
t.Fatal("backstage keyframe request leaked into webcam stream")
|
||||||
|
}
|
||||||
|
if !ConsumeKeyframeRequest(Kindbackstage) {
|
||||||
|
t.Fatal("backstage keyframe request was not retained for backstage stream")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import (
|
|||||||
"github.com/pion/webrtc/v4/pkg/media"
|
"github.com/pion/webrtc/v4/pkg/media"
|
||||||
)
|
)
|
||||||
|
|
||||||
func drainRTCP(sender *webrtc.RTPSender) {
|
func drainRTCP(sender *webrtc.RTPSender, kind Kind) {
|
||||||
buf := make([]byte, 1500)
|
buf := make([]byte, 1500)
|
||||||
for {
|
for {
|
||||||
n, _, err := sender.Read(buf)
|
n, _, err := sender.Read(buf)
|
||||||
@@ -39,7 +39,9 @@ func drainRTCP(sender *webrtc.RTPSender) {
|
|||||||
for _, p := range packets {
|
for _, p := range packets {
|
||||||
switch p.(type) {
|
switch p.(type) {
|
||||||
case *rtcp.PictureLossIndication, *rtcp.FullIntraRequest:
|
case *rtcp.PictureLossIndication, *rtcp.FullIntraRequest:
|
||||||
handleRTCPKeyframeFeedback()
|
if kind != "" {
|
||||||
|
handleRTCPKeyframeFeedback(kind)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -48,11 +50,15 @@ func drainRTCP(sender *webrtc.RTPSender) {
|
|||||||
const h264SDPFmtpLine = "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640034"
|
const h264SDPFmtpLine = "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640034"
|
||||||
|
|
||||||
var rtcpKeyframeLogOnce sync.Once
|
var rtcpKeyframeLogOnce sync.Once
|
||||||
var lastRTCPKeyframeRequest atomic.Int64
|
var (
|
||||||
|
lastDesktopRTCPKeyframeRequest atomic.Int64
|
||||||
|
lastBackstageRTCPKeyframeRequest atomic.Int64
|
||||||
|
lastWebcamRTCPKeyframeRequest atomic.Int64
|
||||||
|
)
|
||||||
|
|
||||||
const rtcpKeyframeMinInterval = 750 * time.Millisecond
|
const rtcpKeyframeMinInterval = 750 * time.Millisecond
|
||||||
|
|
||||||
func handleRTCPKeyframeFeedback() {
|
func handleRTCPKeyframeFeedback(kind Kind) {
|
||||||
if !honorRTCPKeyframes() {
|
if !honorRTCPKeyframes() {
|
||||||
rtcpKeyframeLogOnce.Do(func() {
|
rtcpKeyframeLogOnce.Do(func() {
|
||||||
log.Printf("webrtcpub: RTCP video keyframe recovery disabled by OVERLORD_WEBRTC_RTCP_KEYFRAMES")
|
log.Printf("webrtcpub: RTCP video keyframe recovery disabled by OVERLORD_WEBRTC_RTCP_KEYFRAMES")
|
||||||
@@ -61,15 +67,26 @@ func handleRTCPKeyframeFeedback() {
|
|||||||
}
|
}
|
||||||
now := time.Now().UnixNano()
|
now := time.Now().UnixNano()
|
||||||
for {
|
for {
|
||||||
last := lastRTCPKeyframeRequest.Load()
|
last := rtcpKeyframeTimestamp(kind).Load()
|
||||||
if last > 0 && time.Duration(now-last) < rtcpKeyframeMinInterval {
|
if last > 0 && time.Duration(now-last) < rtcpKeyframeMinInterval {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if lastRTCPKeyframeRequest.CompareAndSwap(last, now) {
|
if rtcpKeyframeTimestamp(kind).CompareAndSwap(last, now) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RequestKeyframe()
|
RequestKeyframe(kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
func rtcpKeyframeTimestamp(kind Kind) *atomic.Int64 {
|
||||||
|
switch kind {
|
||||||
|
case Kindbackstage:
|
||||||
|
return &lastBackstageRTCPKeyframeRequest
|
||||||
|
case KindWebcam:
|
||||||
|
return &lastWebcamRTCPKeyframeRequest
|
||||||
|
default:
|
||||||
|
return &lastDesktopRTCPKeyframeRequest
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func honorRTCPKeyframes() bool {
|
func honorRTCPKeyframes() bool {
|
||||||
@@ -188,7 +205,7 @@ func Start(ctx context.Context, kind Kind, opts Options) (*Publisher, error) {
|
|||||||
return nil, fmt.Errorf("add video transceiver: %w", err)
|
return nil, fmt.Errorf("add video transceiver: %w", err)
|
||||||
}
|
}
|
||||||
if sender := tx.Sender(); sender != nil {
|
if sender := tx.Sender(); sender != nil {
|
||||||
go drainRTCP(sender)
|
go drainRTCP(sender, kind)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if opts.HasAudio {
|
if opts.HasAudio {
|
||||||
@@ -213,7 +230,7 @@ func Start(ctx context.Context, kind Kind, opts Options) (*Publisher, error) {
|
|||||||
return nil, fmt.Errorf("add audio transceiver: %w", err)
|
return nil, fmt.Errorf("add audio transceiver: %w", err)
|
||||||
}
|
}
|
||||||
if sender := tx.Sender(); sender != nil {
|
if sender := tx.Sender(); sender != nil {
|
||||||
go drainRTCP(sender)
|
go drainRTCP(sender, "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user