add async Media Foundation H264 and texture backend abstraction
This commit is contained in:
@@ -7,9 +7,11 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"log"
|
"log"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"time"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"golang.org/x/sys/windows"
|
"golang.org/x/sys/windows"
|
||||||
@@ -23,16 +25,25 @@ const (
|
|||||||
eNotImpl = 0x80004001
|
eNotImpl = 0x80004001
|
||||||
|
|
||||||
mftEnumFlagSyncMFT = 0x00000001
|
mftEnumFlagSyncMFT = 0x00000001
|
||||||
|
mftEnumFlagAsyncMFT = 0x00000002
|
||||||
mftEnumFlagHardware = 0x00000004
|
mftEnumFlagHardware = 0x00000004
|
||||||
mftEnumFlagSortAndFilter = 0x00000040
|
mftEnumFlagSortAndFilter = 0x00000040
|
||||||
|
|
||||||
mftMessageNotifyBeginStreaming = 0x10000000
|
mftMessageNotifyBeginStreaming = 0x10000000
|
||||||
mftMessageNotifyStartOfStream = 0x10000003
|
mftMessageNotifyStartOfStream = 0x10000003
|
||||||
|
mftMessageCommandFlush = 0x00000000
|
||||||
|
mftMessageNotifyEndOfStream = 0x10000002
|
||||||
|
mftMessageNotifyEndStreaming = 0x10000001
|
||||||
|
|
||||||
mftOutputStreamProvidesSamples = 0x00000100
|
mftOutputStreamProvidesSamples = 0x00000100
|
||||||
|
|
||||||
mfETransformNeedMoreInput = 0xC00D6D72
|
mfETransformNeedMoreInput = 0xC00D6D72
|
||||||
mfETransformStreamChange = 0xC00D6D61
|
mfETransformStreamChange = 0xC00D6D61
|
||||||
|
mfENoEventsAvailable = 0xC00D3E80
|
||||||
|
|
||||||
|
meTransformNeedInput = 601
|
||||||
|
meTransformHaveOutput = 602
|
||||||
|
meTransformDrainComplete = 603
|
||||||
|
|
||||||
h264ProfileMain = 77
|
h264ProfileMain = 77
|
||||||
)
|
)
|
||||||
@@ -74,6 +85,12 @@ var (
|
|||||||
MF_MT_MPEG2_LEVEL = windows.GUID{Data1: 0x96f66574, Data2: 0x11c5, Data3: 0x4015, Data4: [8]byte{0x86, 0x66, 0xbf, 0xf5, 0x16, 0x43, 0x6d, 0xa7}}
|
MF_MT_MPEG2_LEVEL = windows.GUID{Data1: 0x96f66574, Data2: 0x11c5, Data3: 0x4015, Data4: [8]byte{0x86, 0x66, 0xbf, 0xf5, 0x16, 0x43, 0x6d, 0xa7}}
|
||||||
MF_MT_MPEG2_PROFILE = windows.GUID{Data1: 0xad76a80b, Data2: 0x2d5c, Data3: 0x4e0b, Data4: [8]byte{0xb3, 0x75, 0x64, 0xe5, 0x20, 0x13, 0x70, 0x36}}
|
MF_MT_MPEG2_PROFILE = windows.GUID{Data1: 0xad76a80b, Data2: 0x2d5c, Data3: 0x4e0b, Data4: [8]byte{0xb3, 0x75, 0x64, 0xe5, 0x20, 0x13, 0x70, 0x36}}
|
||||||
MF_LOW_LATENCY = windows.GUID{Data1: 0x9c27891a, Data2: 0xed7a, Data3: 0x40e1, Data4: [8]byte{0x88, 0xe8, 0xb2, 0x27, 0x27, 0xa0, 0x24, 0xee}}
|
MF_LOW_LATENCY = windows.GUID{Data1: 0x9c27891a, Data2: 0xed7a, Data3: 0x40e1, Data4: [8]byte{0x88, 0xe8, 0xb2, 0x27, 0x27, 0xa0, 0x24, 0xee}}
|
||||||
|
MFT_FRIENDLY_NAME_Attribute = windows.GUID{Data1: 0x314ffbae, Data2: 0x5b41, Data3: 0x4c95, Data4: [8]byte{0x9c, 0x19, 0x4e, 0x7d, 0x58, 0x6f, 0xac, 0xe3}}
|
||||||
|
MFT_ENUM_HARDWARE_URL_Attribute = windows.GUID{Data1: 0x2fb866ac, Data2: 0xb078, Data3: 0x4942, Data4: [8]byte{0xab, 0x6c, 0x00, 0x3d, 0x05, 0xcd, 0xa6, 0x74}}
|
||||||
|
MF_TRANSFORM_ASYNC = windows.GUID{Data1: 0xf81a699a, Data2: 0x649a, Data3: 0x497d, Data4: [8]byte{0x8c, 0x73, 0x29, 0xf8, 0xfe, 0xd6, 0xad, 0x7a}}
|
||||||
|
MF_TRANSFORM_ASYNC_UNLOCK = windows.GUID{Data1: 0xe5666d6b, Data2: 0x3422, Data3: 0x4eb6, Data4: [8]byte{0xa4, 0x21, 0xda, 0x7d, 0xb1, 0xf8, 0xe2, 0x07}}
|
||||||
|
MF_SA_D3D11_AWARE = windows.GUID{Data1: 0x206b4fc8, Data2: 0xfcf9, Data3: 0x4c51, Data4: [8]byte{0xaf, 0xe3, 0x97, 0x64, 0x36, 0x9e, 0x33, 0xa0}}
|
||||||
|
IID_IMFMediaEventGenerator = windows.GUID{Data1: 0x2cd0bd52, Data2: 0xbcd5, Data3: 0x4b89, Data4: [8]byte{0xb6, 0x2c, 0xea, 0xdc, 0x0c, 0x03, 0x1e, 0x7d}}
|
||||||
)
|
)
|
||||||
|
|
||||||
type mfAttributesVtbl struct {
|
type mfAttributesVtbl struct {
|
||||||
@@ -134,6 +151,21 @@ func (a *mfAttributes) SetUINT64(key *windows.GUID, value uint64) uintptr {
|
|||||||
return callSyscallN(a.lpVtbl.SetUINT64, uintptr(unsafe.Pointer(a)), uintptr(unsafe.Pointer(key)), uintptr(value))
|
return callSyscallN(a.lpVtbl.SetUINT64, uintptr(unsafe.Pointer(a)), uintptr(unsafe.Pointer(key)), uintptr(value))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *mfAttributes) GetUINT32(key *windows.GUID, value *uint32) uintptr {
|
||||||
|
return callSyscallN(a.lpVtbl.GetUINT32, uintptr(unsafe.Pointer(a)), uintptr(unsafe.Pointer(key)), uintptr(unsafe.Pointer(value)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *mfAttributes) GetAllocatedString(key *windows.GUID) (string, bool) {
|
||||||
|
var value *uint16
|
||||||
|
var length uint32
|
||||||
|
hr := callSyscallN(a.lpVtbl.GetAllocatedString, uintptr(unsafe.Pointer(a)), uintptr(unsafe.Pointer(key)), uintptr(unsafe.Pointer(&value)), uintptr(unsafe.Pointer(&length)))
|
||||||
|
if failedHR(hr) || value == nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
defer procMFCoTaskMemFree.Call(uintptr(unsafe.Pointer(value)))
|
||||||
|
return windows.UTF16PtrToString(value), true
|
||||||
|
}
|
||||||
|
|
||||||
type mfMediaType struct {
|
type mfMediaType struct {
|
||||||
lpVtbl *mfAttributesVtbl
|
lpVtbl *mfAttributesVtbl
|
||||||
}
|
}
|
||||||
@@ -215,6 +247,56 @@ func (t *mfTransform) ProcessOutput(flags uint32, count uint32, samples *mftOutp
|
|||||||
return callSyscallN(t.lpVtbl.ProcessOutput, uintptr(unsafe.Pointer(t)), uintptr(flags), uintptr(count), uintptr(unsafe.Pointer(samples)), uintptr(unsafe.Pointer(status)))
|
return callSyscallN(t.lpVtbl.ProcessOutput, uintptr(unsafe.Pointer(t)), uintptr(flags), uintptr(count), uintptr(unsafe.Pointer(samples)), uintptr(unsafe.Pointer(status)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type mfMediaEventGeneratorVtbl struct {
|
||||||
|
QueryInterface uintptr
|
||||||
|
AddRef uintptr
|
||||||
|
Release uintptr
|
||||||
|
GetEvent uintptr
|
||||||
|
BeginGetEvent uintptr
|
||||||
|
EndGetEvent uintptr
|
||||||
|
QueueEvent uintptr
|
||||||
|
}
|
||||||
|
|
||||||
|
type mfMediaEventGenerator struct {
|
||||||
|
lpVtbl *mfMediaEventGeneratorVtbl
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *mfMediaEventGenerator) Release() {
|
||||||
|
if g != nil && g.lpVtbl != nil {
|
||||||
|
callSyscallN(g.lpVtbl.Release, uintptr(unsafe.Pointer(g)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *mfMediaEventGenerator) GetEvent(flags uint32, event **mfMediaEvent) uintptr {
|
||||||
|
return callSyscallN(g.lpVtbl.GetEvent, uintptr(unsafe.Pointer(g)), uintptr(flags), uintptr(unsafe.Pointer(event)))
|
||||||
|
}
|
||||||
|
|
||||||
|
type mfMediaEventVtbl struct {
|
||||||
|
mfAttributesVtbl
|
||||||
|
GetType uintptr
|
||||||
|
GetExtendedType uintptr
|
||||||
|
GetStatus uintptr
|
||||||
|
GetValue uintptr
|
||||||
|
}
|
||||||
|
|
||||||
|
type mfMediaEvent struct {
|
||||||
|
lpVtbl *mfMediaEventVtbl
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *mfMediaEvent) Release() {
|
||||||
|
if e != nil && e.lpVtbl != nil {
|
||||||
|
callSyscallN(e.lpVtbl.Release, uintptr(unsafe.Pointer(e)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *mfMediaEvent) GetType(eventType *uint32) uintptr {
|
||||||
|
return callSyscallN(e.lpVtbl.GetType, uintptr(unsafe.Pointer(e)), uintptr(unsafe.Pointer(eventType)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *mfMediaEvent) GetStatus(status *uint32) uintptr {
|
||||||
|
return callSyscallN(e.lpVtbl.GetStatus, uintptr(unsafe.Pointer(e)), uintptr(unsafe.Pointer(status)))
|
||||||
|
}
|
||||||
|
|
||||||
type mfMediaBufferVtbl struct {
|
type mfMediaBufferVtbl struct {
|
||||||
QueryInterface uintptr
|
QueryInterface uintptr
|
||||||
AddRef uintptr
|
AddRef uintptr
|
||||||
@@ -371,6 +453,10 @@ type mfActivate struct {
|
|||||||
lpVtbl *mfActivateVtbl
|
lpVtbl *mfActivateVtbl
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *mfActivate) attrs() *mfAttributes {
|
||||||
|
return (*mfAttributes)(unsafe.Pointer(a))
|
||||||
|
}
|
||||||
|
|
||||||
func (a *mfActivate) Release() {
|
func (a *mfActivate) Release() {
|
||||||
if a != nil && a.lpVtbl != nil {
|
if a != nil && a.lpVtbl != nil {
|
||||||
callSyscallN(a.lpVtbl.Release, uintptr(unsafe.Pointer(a)))
|
callSyscallN(a.lpVtbl.Release, uintptr(unsafe.Pointer(a)))
|
||||||
@@ -412,6 +498,10 @@ type mfH264Encoder struct {
|
|||||||
provider string
|
provider string
|
||||||
inputSubtype windows.GUID
|
inputSubtype windows.GUID
|
||||||
inputFormat string
|
inputFormat string
|
||||||
|
asynchronous bool
|
||||||
|
events *mfMediaEventGenerator
|
||||||
|
inputReady int
|
||||||
|
pendingOut []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
type h264FrameEncoder interface {
|
type h264FrameEncoder interface {
|
||||||
@@ -502,11 +592,11 @@ func h264AvailabilityDetail() string {
|
|||||||
if err := ensureMFStartup(); err != nil {
|
if err := ensureMFStartup(); err != nil {
|
||||||
return fmt.Sprintf("Windows Media Foundation unavailable: %v", err)
|
return fmt.Sprintf("Windows Media Foundation unavailable: %v", err)
|
||||||
}
|
}
|
||||||
ok, reason := hardwareH264MFTStatus()
|
ok, detail := hardwareH264MFTStatus()
|
||||||
if ok {
|
if ok {
|
||||||
return "Windows Media Foundation H.264 MFT (hardware encoder available)"
|
return "Windows Media Foundation H.264 MFT (hardware encoder available: " + detail + ")"
|
||||||
}
|
}
|
||||||
return "Windows Media Foundation H.264 MFT (Microsoft H.264 encoder fallback; hardware unavailable: " + reason + ")"
|
return "Windows Media Foundation H.264 MFT (Microsoft H.264 encoder fallback; hardware unavailable: " + detail + ")"
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetH264TargetFPS(fps int) {
|
func SetH264TargetFPS(fps int) {
|
||||||
@@ -530,7 +620,7 @@ func resetH264Encoder() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func RequestDesktopH264Keyframe() {
|
func RequestDesktopH264Keyframe() {
|
||||||
requestNativeH264D3D11TextureKeyframe()
|
requestH264D3D11TextureKeyframe()
|
||||||
resetH264Encoder()
|
resetH264Encoder()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -566,11 +656,12 @@ func newMFH264Encoder(width, height, fps int) (*mfH264Encoder, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if transform, err := activateHardwareH264MFT(); err == nil && transform != nil {
|
if transform, detail, err := activateHardwareH264MFTDetailed(); err == nil && transform != nil {
|
||||||
log.Printf("capture: media foundation h264 hardware encoder selected")
|
provider := detail.provider()
|
||||||
|
log.Printf("capture: media foundation h264 hardware encoder selected provider=%q", provider)
|
||||||
candidates := hardwareH264Candidates(fps)
|
candidates := hardwareH264Candidates(fps)
|
||||||
for idx, candidate := range candidates {
|
for idx, candidate := range candidates {
|
||||||
enc := newMFH264EncoderFromTransform(transform, width, height, fps, candidate.fps, true, "hardware", candidate.inputSubtype, candidate.inputFormat)
|
enc := newMFH264EncoderFromTransform(transform, width, height, fps, candidate.fps, true, provider, candidate.inputSubtype, candidate.inputFormat)
|
||||||
if err := enc.configure(); err == nil {
|
if err := enc.configure(); err == nil {
|
||||||
logH264EncoderActive(enc)
|
logH264EncoderActive(enc)
|
||||||
return enc, nil
|
return enc, nil
|
||||||
@@ -581,11 +672,13 @@ func newMFH264Encoder(width, height, fps int) (*mfH264Encoder, error) {
|
|||||||
|
|
||||||
if idx != len(candidates)-1 {
|
if idx != len(candidates)-1 {
|
||||||
var nextErr error
|
var nextErr error
|
||||||
transform, nextErr = activateHardwareH264MFT()
|
var nextDetail mfHardwareEncoderDetail
|
||||||
|
transform, nextDetail, nextErr = activateHardwareH264MFTDetailed()
|
||||||
if nextErr != nil {
|
if nextErr != nil {
|
||||||
log.Printf("capture: media foundation h264 hardware encoder reactivation failed after fps retry: %v; using Microsoft software encoder", nextErr)
|
log.Printf("capture: media foundation h264 hardware encoder reactivation failed after fps retry: %v; using Microsoft software encoder", nextErr)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
provider = nextDetail.provider()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.Printf("capture: media foundation h264 hardware encoder rejected all fps candidates for %dx%d requested_fps=%d; using Microsoft software encoder", width, height, fps)
|
log.Printf("capture: media foundation h264 hardware encoder rejected all fps candidates for %dx%d requested_fps=%d; using Microsoft software encoder", width, height, fps)
|
||||||
@@ -672,6 +765,13 @@ func logH264EncoderActive(enc *mfH264Encoder) {
|
|||||||
|
|
||||||
func (e *mfH264Encoder) Close() {
|
func (e *mfH264Encoder) Close() {
|
||||||
if e != nil && e.transform != nil {
|
if e != nil && e.transform != nil {
|
||||||
|
_ = e.transform.ProcessMessage(mftMessageNotifyEndOfStream, 0)
|
||||||
|
_ = e.transform.ProcessMessage(mftMessageCommandFlush, 0)
|
||||||
|
_ = e.transform.ProcessMessage(mftMessageNotifyEndStreaming, 0)
|
||||||
|
if e.events != nil {
|
||||||
|
e.events.Release()
|
||||||
|
e.events = nil
|
||||||
|
}
|
||||||
e.transform.Release()
|
e.transform.Release()
|
||||||
e.transform = nil
|
e.transform = nil
|
||||||
}
|
}
|
||||||
@@ -682,6 +782,9 @@ func (e *mfH264Encoder) Matches(width, height, fps int) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *mfH264Encoder) configure() error {
|
func (e *mfH264Encoder) configure() error {
|
||||||
|
if err := e.configureAsyncMode(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
e.setLowLatency()
|
e.setLowLatency()
|
||||||
|
|
||||||
outType, err := createVideoType(MFVideoFormat_H264, e.width, e.height, e.fps)
|
outType, err := createVideoType(MFVideoFormat_H264, e.width, e.height, e.fps)
|
||||||
@@ -714,6 +817,36 @@ func (e *mfH264Encoder) configure() error {
|
|||||||
if hr := e.transform.ProcessMessage(mftMessageNotifyStartOfStream, 0); failedHR(hr) && hr != eNotImpl {
|
if hr := e.transform.ProcessMessage(mftMessageNotifyStartOfStream, 0); failedHR(hr) && hr != eNotImpl {
|
||||||
return fmt.Errorf("mf h264: start stream failed 0x%x", hr)
|
return fmt.Errorf("mf h264: start stream failed 0x%x", hr)
|
||||||
}
|
}
|
||||||
|
if e.asynchronous {
|
||||||
|
if err := e.waitForAsyncInput(500 * time.Millisecond); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *mfH264Encoder) configureAsyncMode() error {
|
||||||
|
var attrs *mfAttributes
|
||||||
|
hr := e.transform.GetAttributes(&attrs)
|
||||||
|
if failedHR(hr) || attrs == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer attrs.Release()
|
||||||
|
var async uint32
|
||||||
|
_ = attrs.GetUINT32(&MF_TRANSFORM_ASYNC, &async)
|
||||||
|
e.asynchronous = async != 0
|
||||||
|
if !e.asynchronous {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if hr := attrs.SetUINT32(&MF_TRANSFORM_ASYNC_UNLOCK, 1); failedHR(hr) {
|
||||||
|
return fmt.Errorf("mf h264: unlock asynchronous transform failed 0x%x", hr)
|
||||||
|
}
|
||||||
|
if e.events == nil {
|
||||||
|
hr := (*iunknown)(unsafe.Pointer(e.transform)).QueryInterface(&IID_IMFMediaEventGenerator, unsafe.Pointer(&e.events))
|
||||||
|
if failedHR(hr) || e.events == nil {
|
||||||
|
return fmt.Errorf("mf h264: asynchronous transform has no event generator (0x%x)", hr)
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -741,6 +874,9 @@ func (e *mfH264Encoder) Encode(img *image.RGBA) ([]byte, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer sample.Release()
|
defer sample.Release()
|
||||||
|
if e.asynchronous {
|
||||||
|
return e.encodeAsync(sample)
|
||||||
|
}
|
||||||
|
|
||||||
hr := e.transform.ProcessInput(0, sample, 0)
|
hr := e.transform.ProcessInput(0, sample, 0)
|
||||||
if failedHR(hr) {
|
if failedHR(hr) {
|
||||||
@@ -755,6 +891,106 @@ func (e *mfH264Encoder) Encode(img *image.RGBA) ([]byte, error) {
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *mfH264Encoder) encodeAsync(sample *mfSample) ([]byte, error) {
|
||||||
|
if err := e.waitForAsyncInput(500 * time.Millisecond); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
e.inputReady--
|
||||||
|
hr := e.transform.ProcessInput(0, sample, 0)
|
||||||
|
if failedHR(hr) {
|
||||||
|
return nil, fmt.Errorf("mf h264: asynchronous ProcessInput failed 0x%x", hr)
|
||||||
|
}
|
||||||
|
e.frame++
|
||||||
|
|
||||||
|
deadline := time.Now().Add(100 * time.Millisecond)
|
||||||
|
for len(e.pendingOut) == 0 && time.Now().Before(deadline) {
|
||||||
|
hadEvent, err := e.pumpAsyncEvent()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !hadEvent {
|
||||||
|
if e.inputReady > 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
time.Sleep(time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return e.takePendingOutput(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *mfH264Encoder) waitForAsyncInput(timeout time.Duration) error {
|
||||||
|
deadline := time.Now().Add(timeout)
|
||||||
|
for {
|
||||||
|
for {
|
||||||
|
hadEvent, err := e.pumpAsyncEvent()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !hadEvent {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if e.inputReady > 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if time.Now().After(deadline) {
|
||||||
|
return errors.New("mf h264: timed out waiting for asynchronous input request")
|
||||||
|
}
|
||||||
|
time.Sleep(time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *mfH264Encoder) pumpAsyncEvent() (bool, error) {
|
||||||
|
if e.events == nil {
|
||||||
|
return false, errors.New("mf h264: asynchronous event generator is unavailable")
|
||||||
|
}
|
||||||
|
var event *mfMediaEvent
|
||||||
|
hr := e.events.GetEvent(1, &event) // MF_EVENT_FLAG_NO_WAIT
|
||||||
|
if hr == mfENoEventsAvailable {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if failedHR(hr) {
|
||||||
|
return false, fmt.Errorf("mf h264: GetEvent failed 0x%x", hr)
|
||||||
|
}
|
||||||
|
if event == nil {
|
||||||
|
return false, errors.New("mf h264: GetEvent returned a nil event")
|
||||||
|
}
|
||||||
|
defer event.Release()
|
||||||
|
var eventType uint32
|
||||||
|
if hr := event.GetType(&eventType); failedHR(hr) {
|
||||||
|
return false, fmt.Errorf("mf h264: event GetType failed 0x%x", hr)
|
||||||
|
}
|
||||||
|
var eventStatus uint32
|
||||||
|
if hr := event.GetStatus(&eventStatus); failedHR(hr) {
|
||||||
|
return false, fmt.Errorf("mf h264: event GetStatus failed 0x%x", hr)
|
||||||
|
}
|
||||||
|
if failedHR(uintptr(eventStatus)) {
|
||||||
|
return false, fmt.Errorf("mf h264: asynchronous event %d failed 0x%x", eventType, eventStatus)
|
||||||
|
}
|
||||||
|
switch eventType {
|
||||||
|
case meTransformNeedInput:
|
||||||
|
e.inputReady++
|
||||||
|
case meTransformHaveOutput:
|
||||||
|
out, err := e.processOutput()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
e.pendingOut = append(e.pendingOut, out...)
|
||||||
|
case meTransformDrainComplete:
|
||||||
|
// No action is required during normal streaming.
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *mfH264Encoder) takePendingOutput() []byte {
|
||||||
|
if len(e.pendingOut) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := append([]byte(nil), e.pendingOut...)
|
||||||
|
e.pendingOut = e.pendingOut[:0]
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
func (e *mfH264Encoder) processOutput() ([]byte, error) {
|
func (e *mfH264Encoder) processOutput() ([]byte, error) {
|
||||||
var info mftOutputStreamInfo
|
var info mftOutputStreamInfo
|
||||||
hr := e.transform.GetOutputStreamInfo(0, &info)
|
hr := e.transform.GetOutputStreamInfo(0, &info)
|
||||||
@@ -800,6 +1036,9 @@ func (e *mfH264Encoder) processOutput() ([]byte, error) {
|
|||||||
if output.sample == nil {
|
if output.sample == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
if outSample == nil {
|
||||||
|
defer output.sample.Release()
|
||||||
|
}
|
||||||
return sampleBytes(output.sample)
|
return sampleBytes(output.sample)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -925,9 +1164,10 @@ func sampleBytes(sample *mfSample) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func createH264Transform() (*mfTransform, bool, string, error) {
|
func createH264Transform() (*mfTransform, bool, string, error) {
|
||||||
if transform, err := activateHardwareH264MFT(); err == nil && transform != nil {
|
if transform, detail, err := activateHardwareH264MFTDetailed(); err == nil && transform != nil {
|
||||||
log.Printf("capture: media foundation h264 hardware encoder selected")
|
provider := detail.provider()
|
||||||
return transform, true, "hardware", nil
|
log.Printf("capture: media foundation h264 hardware encoder selected provider=%q", provider)
|
||||||
|
return transform, true, provider, nil
|
||||||
} else {
|
} else {
|
||||||
log.Printf("capture: media foundation h264 hardware encoder unavailable: %v; using Microsoft software encoder", err)
|
log.Printf("capture: media foundation h264 hardware encoder unavailable: %v; using Microsoft software encoder", err)
|
||||||
}
|
}
|
||||||
@@ -949,52 +1189,86 @@ func createSoftwareH264Transform() (*mfTransform, bool, string, error) {
|
|||||||
return transform, false, "Microsoft software", nil
|
return transform, false, "Microsoft software", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type mfHardwareEncoderDetail struct {
|
||||||
|
name string
|
||||||
|
hardwareURL string
|
||||||
|
asynchronous bool
|
||||||
|
d3d11Aware bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d mfHardwareEncoderDetail) provider() string {
|
||||||
|
if strings.TrimSpace(d.name) != "" {
|
||||||
|
return d.name
|
||||||
|
}
|
||||||
|
return "Media Foundation hardware"
|
||||||
|
}
|
||||||
|
|
||||||
func activateHardwareH264MFT() (*mfTransform, error) {
|
func activateHardwareH264MFT() (*mfTransform, error) {
|
||||||
|
transform, _, err := activateHardwareH264MFTDetailed()
|
||||||
|
return transform, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func activateHardwareH264MFTDetailed() (*mfTransform, mfHardwareEncoderDetail, error) {
|
||||||
input := mftRegisterTypeInfo{majorType: MFMediaType_Video, subtype: MFVideoFormat_NV12}
|
input := mftRegisterTypeInfo{majorType: MFMediaType_Video, subtype: MFVideoFormat_NV12}
|
||||||
output := mftRegisterTypeInfo{majorType: MFMediaType_Video, subtype: MFVideoFormat_H264}
|
output := mftRegisterTypeInfo{majorType: MFMediaType_Video, subtype: MFVideoFormat_H264}
|
||||||
var activates **mfActivate
|
var activates **mfActivate
|
||||||
var count uint32
|
var count uint32
|
||||||
hr, _, _ := procMFTEnumEx.Call(
|
hr, _, _ := procMFTEnumEx.Call(
|
||||||
uintptr(unsafe.Pointer(&MFT_CATEGORY_VIDEO_ENCODER)),
|
uintptr(unsafe.Pointer(&MFT_CATEGORY_VIDEO_ENCODER)),
|
||||||
uintptr(mftEnumFlagSyncMFT|mftEnumFlagHardware|mftEnumFlagSortAndFilter),
|
uintptr(mftEnumFlagSyncMFT|mftEnumFlagAsyncMFT|mftEnumFlagHardware|mftEnumFlagSortAndFilter),
|
||||||
uintptr(unsafe.Pointer(&input)),
|
uintptr(unsafe.Pointer(&input)),
|
||||||
uintptr(unsafe.Pointer(&output)),
|
uintptr(unsafe.Pointer(&output)),
|
||||||
uintptr(unsafe.Pointer(&activates)),
|
uintptr(unsafe.Pointer(&activates)),
|
||||||
uintptr(unsafe.Pointer(&count)),
|
uintptr(unsafe.Pointer(&count)),
|
||||||
)
|
)
|
||||||
if failedHR(hr) || count == 0 || activates == nil {
|
if failedHR(hr) || count == 0 || activates == nil {
|
||||||
return nil, fmt.Errorf("mf h264: no synchronous hardware encoder (0x%x)", hr)
|
return nil, mfHardwareEncoderDetail{}, fmt.Errorf("mf h264: no hardware encoder (0x%x)", hr)
|
||||||
}
|
}
|
||||||
defer procMFCoTaskMemFree.Call(uintptr(unsafe.Pointer(activates)))
|
defer procMFCoTaskMemFree.Call(uintptr(unsafe.Pointer(activates)))
|
||||||
|
|
||||||
list := unsafe.Slice(activates, int(count))
|
list := unsafe.Slice(activates, int(count))
|
||||||
var found *mfTransform
|
var found *mfTransform
|
||||||
|
var foundDetail mfHardwareEncoderDetail
|
||||||
for _, activate := range list {
|
for _, activate := range list {
|
||||||
if activate == nil {
|
if activate == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
detail := mfHardwareEncoderDetail{}
|
||||||
|
detail.name, _ = activate.attrs().GetAllocatedString(&MFT_FRIENDLY_NAME_Attribute)
|
||||||
|
detail.hardwareURL, _ = activate.attrs().GetAllocatedString(&MFT_ENUM_HARDWARE_URL_Attribute)
|
||||||
if found == nil {
|
if found == nil {
|
||||||
var transform *mfTransform
|
var transform *mfTransform
|
||||||
hr := activate.ActivateObject(&IID_IMFTransform, unsafe.Pointer(&transform))
|
hr := activate.ActivateObject(&IID_IMFTransform, unsafe.Pointer(&transform))
|
||||||
if !failedHR(hr) && transform != nil {
|
if !failedHR(hr) && transform != nil {
|
||||||
|
var attrs *mfAttributes
|
||||||
|
if !failedHR(transform.GetAttributes(&attrs)) && attrs != nil {
|
||||||
|
var async, d3d11 uint32
|
||||||
|
_ = attrs.GetUINT32(&MF_TRANSFORM_ASYNC, &async)
|
||||||
|
_ = attrs.GetUINT32(&MF_SA_D3D11_AWARE, &d3d11)
|
||||||
|
detail.asynchronous = async != 0
|
||||||
|
detail.d3d11Aware = d3d11 != 0
|
||||||
|
attrs.Release()
|
||||||
|
}
|
||||||
found = transform
|
found = transform
|
||||||
|
foundDetail = detail
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
activate.Release()
|
activate.Release()
|
||||||
}
|
}
|
||||||
if found != nil {
|
if found != nil {
|
||||||
return found, nil
|
log.Printf("capture: media foundation hardware h264 candidate selected name=%q asynchronous=%t d3d11_aware=%t hardware_url=%q", foundDetail.provider(), foundDetail.asynchronous, foundDetail.d3d11Aware, foundDetail.hardwareURL)
|
||||||
|
return found, foundDetail, nil
|
||||||
}
|
}
|
||||||
return nil, errors.New("mf h264: hardware encoder activation failed")
|
return nil, mfHardwareEncoderDetail{}, errors.New("mf h264: hardware encoder activation failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
func hardwareH264MFTStatus() (bool, string) {
|
func hardwareH264MFTStatus() (bool, string) {
|
||||||
transform, err := activateHardwareH264MFT()
|
transform, detail, err := activateHardwareH264MFTDetailed()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err.Error()
|
return false, err.Error()
|
||||||
}
|
}
|
||||||
transform.Release()
|
transform.Release()
|
||||||
return true, ""
|
return true, detail.provider()
|
||||||
}
|
}
|
||||||
|
|
||||||
func ensureMFStartup() error {
|
func ensureMFStartup() error {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"time"
|
"time"
|
||||||
|
"unsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
type H264SmokeResolution struct {
|
type H264SmokeResolution struct {
|
||||||
@@ -39,6 +40,72 @@ type H264SmokeResult struct {
|
|||||||
AvgBytes float64 `json:"avg_bytes"`
|
AvgBytes float64 `json:"avg_bytes"`
|
||||||
TotalBytes int `json:"total_bytes"`
|
TotalBytes int `json:"total_bytes"`
|
||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
|
Asynchronous bool `json:"asynchronous"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type H264MFTFinding struct {
|
||||||
|
Index int `json:"index"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
HardwareURL string `json:"hardware_url,omitempty"`
|
||||||
|
Asynchronous bool `json:"asynchronous"`
|
||||||
|
D3D11Aware bool `json:"d3d11_aware"`
|
||||||
|
ActivationOK bool `json:"activation_ok"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func FindH264HardwareMFTs() ([]H264MFTFinding, error) {
|
||||||
|
if err := ensureMFStartup(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
input := mftRegisterTypeInfo{majorType: MFMediaType_Video, subtype: MFVideoFormat_NV12}
|
||||||
|
output := mftRegisterTypeInfo{majorType: MFMediaType_Video, subtype: MFVideoFormat_H264}
|
||||||
|
var activates **mfActivate
|
||||||
|
var count uint32
|
||||||
|
hr, _, _ := procMFTEnumEx.Call(
|
||||||
|
uintptr(unsafe.Pointer(&MFT_CATEGORY_VIDEO_ENCODER)),
|
||||||
|
uintptr(mftEnumFlagSyncMFT|mftEnumFlagAsyncMFT|mftEnumFlagHardware|mftEnumFlagSortAndFilter),
|
||||||
|
uintptr(unsafe.Pointer(&input)), uintptr(unsafe.Pointer(&output)),
|
||||||
|
uintptr(unsafe.Pointer(&activates)), uintptr(unsafe.Pointer(&count)),
|
||||||
|
)
|
||||||
|
if failedHR(hr) {
|
||||||
|
return nil, fmt.Errorf("MFTEnumEx failed 0x%x", hr)
|
||||||
|
}
|
||||||
|
if activates == nil || count == 0 {
|
||||||
|
return []H264MFTFinding{}, nil
|
||||||
|
}
|
||||||
|
defer procMFCoTaskMemFree.Call(uintptr(unsafe.Pointer(activates)))
|
||||||
|
list := unsafe.Slice(activates, int(count))
|
||||||
|
findings := make([]H264MFTFinding, 0, len(list))
|
||||||
|
for index, activate := range list {
|
||||||
|
finding := H264MFTFinding{Index: index}
|
||||||
|
if activate == nil {
|
||||||
|
finding.Error = "nil activation object"
|
||||||
|
findings = append(findings, finding)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
finding.Name, _ = activate.attrs().GetAllocatedString(&MFT_FRIENDLY_NAME_Attribute)
|
||||||
|
finding.HardwareURL, _ = activate.attrs().GetAllocatedString(&MFT_ENUM_HARDWARE_URL_Attribute)
|
||||||
|
var transform *mfTransform
|
||||||
|
hr := activate.ActivateObject(&IID_IMFTransform, unsafe.Pointer(&transform))
|
||||||
|
if failedHR(hr) || transform == nil {
|
||||||
|
finding.Error = fmt.Sprintf("ActivateObject failed 0x%x", hr)
|
||||||
|
} else {
|
||||||
|
finding.ActivationOK = true
|
||||||
|
var attrs *mfAttributes
|
||||||
|
if !failedHR(transform.GetAttributes(&attrs)) && attrs != nil {
|
||||||
|
var async, d3d11 uint32
|
||||||
|
_ = attrs.GetUINT32(&MF_TRANSFORM_ASYNC, &async)
|
||||||
|
_ = attrs.GetUINT32(&MF_SA_D3D11_AWARE, &d3d11)
|
||||||
|
finding.Asynchronous = async != 0
|
||||||
|
finding.D3D11Aware = d3d11 != 0
|
||||||
|
attrs.Release()
|
||||||
|
}
|
||||||
|
transform.Release()
|
||||||
|
}
|
||||||
|
activate.Release()
|
||||||
|
findings = append(findings, finding)
|
||||||
|
}
|
||||||
|
return findings, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func DefaultH264SmokeOptions() H264SmokeOptions {
|
func DefaultH264SmokeOptions() H264SmokeOptions {
|
||||||
@@ -125,9 +192,11 @@ func runH264SmokeCase(provider string, width, height, requestedFPS int, candidat
|
|||||||
enc := newMFH264EncoderFromTransform(transform, width, height, requestedFPS, candidate.fps, hardware, providerName, candidate.inputSubtype, candidate.inputFormat)
|
enc := newMFH264EncoderFromTransform(transform, width, height, requestedFPS, candidate.fps, hardware, providerName, candidate.inputSubtype, candidate.inputFormat)
|
||||||
|
|
||||||
t0 := time.Now()
|
t0 := time.Now()
|
||||||
if err := enc.configure(); err != nil {
|
configureErr := enc.configure()
|
||||||
|
result.Asynchronous = enc.asynchronous
|
||||||
|
if configureErr != nil {
|
||||||
result.ConfigureMS = float64(time.Since(t0).Microseconds()) / 1000
|
result.ConfigureMS = float64(time.Since(t0).Microseconds()) / 1000
|
||||||
result.Error = err.Error()
|
result.Error = configureErr.Error()
|
||||||
enc.Close()
|
enc.Close()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -179,11 +248,11 @@ func runH264SmokeCase(provider string, width, height, requestedFPS int, candidat
|
|||||||
func createH264SmokeTransform(provider string) (*mfTransform, bool, string, error) {
|
func createH264SmokeTransform(provider string) (*mfTransform, bool, string, error) {
|
||||||
switch provider {
|
switch provider {
|
||||||
case "hardware":
|
case "hardware":
|
||||||
transform, err := activateHardwareH264MFT()
|
transform, detail, err := activateHardwareH264MFTDetailed()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, "hardware", err
|
return nil, false, "hardware", err
|
||||||
}
|
}
|
||||||
return transform, true, "hardware", nil
|
return transform, true, detail.provider(), nil
|
||||||
case "software":
|
case "software":
|
||||||
return createSoftwareH264Transform()
|
return createSoftwareH264Transform()
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package capture
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
type h264D3D11TextureRequest struct {
|
||||||
|
Device unsafe.Pointer
|
||||||
|
Texture unsafe.Pointer
|
||||||
|
InputWidth int
|
||||||
|
InputHeight int
|
||||||
|
EncodeWidth int
|
||||||
|
EncodeHeight int
|
||||||
|
FPS int
|
||||||
|
DXGIFormat uint32
|
||||||
|
ForceIDR bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type h264D3D11TextureBackend interface {
|
||||||
|
Name() string
|
||||||
|
Encode(h264D3D11TextureRequest) ([]byte, error)
|
||||||
|
RequestKeyframe()
|
||||||
|
Reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
type nvencD3D11TextureBackend struct{}
|
||||||
|
|
||||||
|
func (nvencD3D11TextureBackend) Name() string { return "NVIDIA NVENC" }
|
||||||
|
|
||||||
|
func (nvencD3D11TextureBackend) Encode(req h264D3D11TextureRequest) ([]byte, error) {
|
||||||
|
return encodeNativeH264D3D11Texture(req.Device, req.Texture, req.InputWidth, req.InputHeight,
|
||||||
|
req.EncodeWidth, req.EncodeHeight, req.FPS, req.DXGIFormat, req.ForceIDR)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nvencD3D11TextureBackend) RequestKeyframe() {
|
||||||
|
requestNativeH264D3D11TextureKeyframe()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (nvencD3D11TextureBackend) Reset() {
|
||||||
|
resetNativeH264D3D11TextureEncoder()
|
||||||
|
}
|
||||||
|
|
||||||
|
var d3d11H264TextureRegistry = struct {
|
||||||
|
sync.Mutex
|
||||||
|
backends []h264D3D11TextureBackend
|
||||||
|
active h264D3D11TextureBackend
|
||||||
|
}{
|
||||||
|
backends: []h264D3D11TextureBackend{nvencD3D11TextureBackend{}},
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeH264D3D11Texture(req h264D3D11TextureRequest) ([]byte, string, error) {
|
||||||
|
if req.Device == nil || req.Texture == nil {
|
||||||
|
return nil, "", fmt.Errorf("nil D3D11 device or texture")
|
||||||
|
}
|
||||||
|
d3d11H264TextureRegistry.Lock()
|
||||||
|
defer d3d11H264TextureRegistry.Unlock()
|
||||||
|
|
||||||
|
failedActiveName := ""
|
||||||
|
errorsByBackend := make([]string, 0, len(d3d11H264TextureRegistry.backends))
|
||||||
|
if active := d3d11H264TextureRegistry.active; active != nil {
|
||||||
|
out, err := active.Encode(req)
|
||||||
|
if err == nil {
|
||||||
|
return out, active.Name(), nil
|
||||||
|
}
|
||||||
|
active.Reset()
|
||||||
|
failedActiveName = active.Name()
|
||||||
|
errorsByBackend = append(errorsByBackend, active.Name()+": "+err.Error())
|
||||||
|
d3d11H264TextureRegistry.active = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, backend := range d3d11H264TextureRegistry.backends {
|
||||||
|
if backend == nil || (failedActiveName != "" && backend.Name() == failedActiveName) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out, err := backend.Encode(req)
|
||||||
|
if err == nil {
|
||||||
|
d3d11H264TextureRegistry.active = backend
|
||||||
|
return out, backend.Name(), nil
|
||||||
|
}
|
||||||
|
backend.Reset()
|
||||||
|
errorsByBackend = append(errorsByBackend, backend.Name()+": "+err.Error())
|
||||||
|
}
|
||||||
|
if len(errorsByBackend) == 0 {
|
||||||
|
return nil, "", fmt.Errorf("no D3D11 H.264 texture encoder backends registered")
|
||||||
|
}
|
||||||
|
return nil, "", fmt.Errorf("D3D11 H.264 texture encoders unavailable: %s", strings.Join(errorsByBackend, "; "))
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestH264D3D11TextureKeyframe() {
|
||||||
|
d3d11H264TextureRegistry.Lock()
|
||||||
|
defer d3d11H264TextureRegistry.Unlock()
|
||||||
|
if d3d11H264TextureRegistry.active != nil {
|
||||||
|
d3d11H264TextureRegistry.active.RequestKeyframe()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, backend := range d3d11H264TextureRegistry.backends {
|
||||||
|
if backend != nil {
|
||||||
|
backend.RequestKeyframe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetH264D3D11TextureEncoder() {
|
||||||
|
d3d11H264TextureRegistry.Lock()
|
||||||
|
defer d3d11H264TextureRegistry.Unlock()
|
||||||
|
for _, backend := range d3d11H264TextureRegistry.backends {
|
||||||
|
if backend != nil {
|
||||||
|
backend.Reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d3d11H264TextureRegistry.active = nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package capture
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fakeD3D11TextureBackend struct {
|
||||||
|
name string
|
||||||
|
outputs [][]byte
|
||||||
|
errors []error
|
||||||
|
encodeCalls int
|
||||||
|
resetCalls int
|
||||||
|
keyframeCalls int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *fakeD3D11TextureBackend) Name() string { return b.name }
|
||||||
|
func (b *fakeD3D11TextureBackend) Encode(h264D3D11TextureRequest) ([]byte, error) {
|
||||||
|
index := b.encodeCalls
|
||||||
|
b.encodeCalls++
|
||||||
|
if index < len(b.errors) && b.errors[index] != nil {
|
||||||
|
return nil, b.errors[index]
|
||||||
|
}
|
||||||
|
if index < len(b.outputs) {
|
||||||
|
return b.outputs[index], nil
|
||||||
|
}
|
||||||
|
return []byte{byte(index + 1)}, nil
|
||||||
|
}
|
||||||
|
func (b *fakeD3D11TextureBackend) RequestKeyframe() { b.keyframeCalls++ }
|
||||||
|
func (b *fakeD3D11TextureBackend) Reset() { b.resetCalls++ }
|
||||||
|
|
||||||
|
func TestD3D11TextureBackendSelectionAndFailover(t *testing.T) {
|
||||||
|
first := &fakeD3D11TextureBackend{name: "first", errors: []error{errors.New("unsupported")}}
|
||||||
|
second := &fakeD3D11TextureBackend{name: "second", outputs: [][]byte{{1, 2, 3}, {4, 5, 6}}}
|
||||||
|
|
||||||
|
d3d11H264TextureRegistry.Lock()
|
||||||
|
savedBackends := d3d11H264TextureRegistry.backends
|
||||||
|
savedActive := d3d11H264TextureRegistry.active
|
||||||
|
d3d11H264TextureRegistry.backends = []h264D3D11TextureBackend{first, second}
|
||||||
|
d3d11H264TextureRegistry.active = nil
|
||||||
|
d3d11H264TextureRegistry.Unlock()
|
||||||
|
t.Cleanup(func() {
|
||||||
|
d3d11H264TextureRegistry.Lock()
|
||||||
|
d3d11H264TextureRegistry.backends = savedBackends
|
||||||
|
d3d11H264TextureRegistry.active = savedActive
|
||||||
|
d3d11H264TextureRegistry.Unlock()
|
||||||
|
})
|
||||||
|
|
||||||
|
device, texture := 1, 2
|
||||||
|
req := h264D3D11TextureRequest{Device: unsafe.Pointer(&device), Texture: unsafe.Pointer(&texture)}
|
||||||
|
out, provider, err := encodeH264D3D11Texture(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("first encode: %v", err)
|
||||||
|
}
|
||||||
|
if provider != "second" || len(out) != 3 {
|
||||||
|
t.Fatalf("provider=%q out=%v", provider, out)
|
||||||
|
}
|
||||||
|
if first.resetCalls != 1 || second.encodeCalls != 1 {
|
||||||
|
t.Fatalf("unexpected calls first.reset=%d second.encode=%d", first.resetCalls, second.encodeCalls)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, provider, err = encodeH264D3D11Texture(req)
|
||||||
|
if err != nil || provider != "second" {
|
||||||
|
t.Fatalf("active backend reuse provider=%q err=%v", provider, err)
|
||||||
|
}
|
||||||
|
if first.encodeCalls != 1 || second.encodeCalls != 2 {
|
||||||
|
t.Fatalf("active backend was not reused: first=%d second=%d", first.encodeCalls, second.encodeCalls)
|
||||||
|
}
|
||||||
|
|
||||||
|
requestH264D3D11TextureKeyframe()
|
||||||
|
if second.keyframeCalls != 1 || first.keyframeCalls != 0 {
|
||||||
|
t.Fatalf("keyframe routed incorrectly: first=%d second=%d", first.keyframeCalls, second.keyframeCalls)
|
||||||
|
}
|
||||||
|
resetH264D3D11TextureEncoder()
|
||||||
|
if first.resetCalls != 2 || second.resetCalls != 1 {
|
||||||
|
t.Fatalf("reset did not reach every backend: first=%d second=%d", first.resetCalls, second.resetCalls)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -665,7 +665,7 @@ func (s *duplicationState) closeLocked() {
|
|||||||
s.h264LastTex = nil
|
s.h264LastTex = nil
|
||||||
s.h264LastDesc = d3d11Texture2DDesc{}
|
s.h264LastDesc = d3d11Texture2DDesc{}
|
||||||
}
|
}
|
||||||
resetNativeH264D3D11TextureEncoder()
|
resetH264D3D11TextureEncoder()
|
||||||
if s.context != nil {
|
if s.context != nil {
|
||||||
s.context.Release()
|
s.context.Release()
|
||||||
s.context = nil
|
s.context = nil
|
||||||
@@ -887,7 +887,7 @@ func (s *duplicationState) captureH264(display int, forceKeyframe bool) ([]byte,
|
|||||||
return nil, 0, 0, 0, 0, false, fmt.Errorf("direct h264 computed invalid scaled output %dx%d", encodeW, encodeH)
|
return nil, 0, 0, 0, 0, false, fmt.Errorf("direct h264 computed invalid scaled output %dx%d", encodeW, encodeH)
|
||||||
}
|
}
|
||||||
directH264ScaleOnce.Do(func() {
|
directH264ScaleOnce.Do(func() {
|
||||||
log.Printf("capture: direct DXGI->NVENC h264 using NVENC scale=%.2f input=%dx%d output=%dx%d", scale, width, height, encodeW, encodeH)
|
log.Printf("capture: direct DXGI hardware h264 scaling enabled scale=%.2f input=%dx%d output=%dx%d", scale, width, height, encodeW, encodeH)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -899,11 +899,15 @@ func (s *duplicationState) captureH264(display int, forceKeyframe bool) ([]byte,
|
|||||||
captureDur := time.Since(capStart)
|
captureDur := time.Since(capStart)
|
||||||
fps := activeH264FPS()
|
fps := activeH264FPS()
|
||||||
encStart := time.Now()
|
encStart := time.Now()
|
||||||
out, err := encodeNativeH264D3D11Texture(unsafe.Pointer(s.device), unsafe.Pointer(s.h264LastTex), width, height, encodeW, encodeH, fps, s.h264LastDesc.Format, forceKeyframe)
|
out, _, err := encodeH264D3D11Texture(h264D3D11TextureRequest{
|
||||||
|
Device: unsafe.Pointer(s.device), Texture: unsafe.Pointer(s.h264LastTex),
|
||||||
|
InputWidth: width, InputHeight: height, EncodeWidth: encodeW, EncodeHeight: encodeH,
|
||||||
|
FPS: fps, DXGIFormat: s.h264LastDesc.Format, ForceIDR: forceKeyframe,
|
||||||
|
})
|
||||||
encodeDur := time.Since(encStart)
|
encodeDur := time.Since(encStart)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
directH264WarnOnce.Do(func() {
|
directH264WarnOnce.Do(func() {
|
||||||
log.Printf("capture: direct DXGI->NVENC h264 cached-frame encode failed for %dx%d@%dfps format=%d: %v; using readback path", width, height, fps, s.h264LastDesc.Format, err)
|
log.Printf("capture: direct DXGI hardware h264 cached-frame encode failed for %dx%d@%dfps format=%d: %v; using readback path", width, height, fps, s.h264LastDesc.Format, err)
|
||||||
})
|
})
|
||||||
return nil, 0, 0, 0, 0, false, err
|
return nil, 0, 0, 0, 0, false, err
|
||||||
}
|
}
|
||||||
@@ -955,13 +959,13 @@ func (s *duplicationState) captureH264(display int, forceKeyframe bool) ([]byte,
|
|||||||
}
|
}
|
||||||
if srcDesc.Format != dxgiFormatB8G8R8A8UNorm && srcDesc.Format != dxgiFormatR8G8B8A8UNorm {
|
if srcDesc.Format != dxgiFormatB8G8R8A8UNorm && srcDesc.Format != dxgiFormatR8G8B8A8UNorm {
|
||||||
directH264WarnOnce.Do(func() {
|
directH264WarnOnce.Do(func() {
|
||||||
log.Printf("capture: direct DXGI->NVENC h264 unavailable: unsupported DXGI texture format %d; using readback path", srcDesc.Format)
|
log.Printf("capture: direct DXGI hardware h264 unavailable: unsupported DXGI texture format %d; using readback path", srcDesc.Format)
|
||||||
})
|
})
|
||||||
return nil, 0, 0, 0, 0, false, fmt.Errorf("direct h264 unsupported DXGI texture format %d", srcDesc.Format)
|
return nil, 0, 0, 0, 0, false, fmt.Errorf("direct h264 unsupported DXGI texture format %d", srcDesc.Format)
|
||||||
}
|
}
|
||||||
if err := s.cacheH264TextureLocked(tex, srcDesc); err != nil {
|
if err := s.cacheH264TextureLocked(tex, srcDesc); err != nil {
|
||||||
directH264WarnOnce.Do(func() {
|
directH264WarnOnce.Do(func() {
|
||||||
log.Printf("capture: direct DXGI->NVENC h264 cache setup failed for %dx%d format=%d: %v; using readback path", width, height, srcDesc.Format, err)
|
log.Printf("capture: direct DXGI hardware h264 cache setup failed for %dx%d format=%d: %v; using readback path", width, height, srcDesc.Format, err)
|
||||||
})
|
})
|
||||||
return nil, 0, 0, 0, 0, false, err
|
return nil, 0, 0, 0, 0, false, err
|
||||||
}
|
}
|
||||||
@@ -972,11 +976,15 @@ func (s *duplicationState) captureH264(display int, forceKeyframe bool) ([]byte,
|
|||||||
|
|
||||||
fps := activeH264FPS()
|
fps := activeH264FPS()
|
||||||
encStart := time.Now()
|
encStart := time.Now()
|
||||||
out, err := encodeNativeH264D3D11Texture(unsafe.Pointer(s.device), unsafe.Pointer(s.h264LastTex), width, height, encodeW, encodeH, fps, s.h264LastDesc.Format, forceKeyframe)
|
out, provider, err := encodeH264D3D11Texture(h264D3D11TextureRequest{
|
||||||
|
Device: unsafe.Pointer(s.device), Texture: unsafe.Pointer(s.h264LastTex),
|
||||||
|
InputWidth: width, InputHeight: height, EncodeWidth: encodeW, EncodeHeight: encodeH,
|
||||||
|
FPS: fps, DXGIFormat: s.h264LastDesc.Format, ForceIDR: forceKeyframe,
|
||||||
|
})
|
||||||
encodeDur := time.Since(encStart)
|
encodeDur := time.Since(encStart)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
directH264WarnOnce.Do(func() {
|
directH264WarnOnce.Do(func() {
|
||||||
log.Printf("capture: direct DXGI->NVENC h264 failed for %dx%d@%dfps format=%d: %v; using readback path", width, height, fps, s.h264LastDesc.Format, err)
|
log.Printf("capture: direct DXGI hardware h264 failed for %dx%d@%dfps format=%d: %v; using readback path", width, height, fps, s.h264LastDesc.Format, err)
|
||||||
})
|
})
|
||||||
return nil, 0, 0, 0, 0, false, err
|
return nil, 0, 0, 0, 0, false, err
|
||||||
}
|
}
|
||||||
@@ -984,7 +992,7 @@ func (s *duplicationState) captureH264(display int, forceKeyframe bool) ([]byte,
|
|||||||
return nil, encodeW, encodeH, captureDur, encodeDur, true, nil
|
return nil, encodeW, encodeH, captureDur, encodeDur, true, nil
|
||||||
}
|
}
|
||||||
directH264ActiveOnce.Do(func() {
|
directH264ActiveOnce.Do(func() {
|
||||||
log.Printf("capture: direct DXGI->NVENC h264 path active input=%dx%d output=%dx%d fps=%d dxgi_format=%d", width, height, encodeW, encodeH, fps, srcDesc.Format)
|
log.Printf("capture: direct DXGI hardware h264 path active provider=%q input=%dx%d output=%dx%d fps=%d dxgi_format=%d", provider, width, height, encodeW, encodeH, fps, srcDesc.Format)
|
||||||
})
|
})
|
||||||
return out, encodeW, encodeH, captureDur, encodeDur, true, nil
|
return out, encodeW, encodeH, captureDur, encodeDur, true, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,36 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"overlord-client/cmd/agent/capture"
|
"overlord-client/cmd/agent/capture"
|
||||||
|
"overlord-client/cmd/agent/sysinfo"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type machineInfo struct {
|
||||||
|
Hostname string `json:"hostname"`
|
||||||
|
OS string `json:"os"`
|
||||||
|
Arch string `json:"arch"`
|
||||||
|
CPU string `json:"cpu"`
|
||||||
|
GPU string `json:"gpu"`
|
||||||
|
RAM string `json:"ram"`
|
||||||
|
GeneratedAt string `json:"generated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type smokeReport struct {
|
||||||
|
Machine machineInfo `json:"machine"`
|
||||||
|
Findings []capture.H264MFTFinding `json:"media_foundation_findings"`
|
||||||
|
FindingError string `json:"media_foundation_finding_error,omitempty"`
|
||||||
|
H264 []capture.H264SmokeResult `json:"h264"`
|
||||||
|
Capture []capture.CaptureSmokeResult `json:"capture,omitempty"`
|
||||||
|
NVENC *capture.NVENCSmokeResult `json:"nvenc,omitempty"`
|
||||||
|
NVENCD3D11 []capture.NVENCD3D11SmokeResult `json:"nvenc_d3d11,omitempty"`
|
||||||
|
Nvidia []string `json:"nvidia_smi,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var frames int
|
var frames int
|
||||||
var captureFrames int
|
var captureFrames int
|
||||||
@@ -27,6 +51,7 @@ func main() {
|
|||||||
var resArg string
|
var resArg string
|
||||||
var fpsArg string
|
var fpsArg string
|
||||||
var providersArg string
|
var providersArg string
|
||||||
|
var reportPath string
|
||||||
flag.IntVar(&frames, "frames", 30, "frames to feed each encoder case")
|
flag.IntVar(&frames, "frames", 30, "frames to feed each encoder case")
|
||||||
flag.IntVar(&captureFrames, "capture-frames", 30, "frames to capture for each capture backend")
|
flag.IntVar(&captureFrames, "capture-frames", 30, "frames to capture for each capture backend")
|
||||||
flag.IntVar(&captureDisplay, "display", 0, "display index for capture smoke")
|
flag.IntVar(&captureDisplay, "display", 0, "display index for capture smoke")
|
||||||
@@ -39,7 +64,12 @@ func main() {
|
|||||||
flag.StringVar(&resArg, "res", "1280x720,1920x1080,2560x1440,3840x2160", "comma-separated resolutions")
|
flag.StringVar(&resArg, "res", "1280x720,1920x1080,2560x1440,3840x2160", "comma-separated resolutions")
|
||||||
flag.StringVar(&fpsArg, "fps", "30,60,120", "comma-separated requested FPS values")
|
flag.StringVar(&fpsArg, "fps", "30,60,120", "comma-separated requested FPS values")
|
||||||
flag.StringVar(&providersArg, "providers", "hardware,software", "comma-separated providers: hardware,software")
|
flag.StringVar(&providersArg, "providers", "hardware,software", "comma-separated providers: hardware,software")
|
||||||
|
flag.StringVar(&reportPath, "report", "", "also write the complete JSON report to this file")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
hostname, _ := os.Hostname()
|
||||||
|
system := sysinfo.Collect()
|
||||||
|
machine := machineInfo{Hostname: hostname, OS: sysinfo.OSName(), Arch: runtime.GOARCH, CPU: system.CPU, GPU: system.GPU, RAM: system.RAM, GeneratedAt: time.Now().Format(time.RFC3339)}
|
||||||
|
findings, findingErr := capture.FindH264HardwareMFTs()
|
||||||
|
|
||||||
opts := capture.DefaultH264SmokeOptions()
|
opts := capture.DefaultH264SmokeOptions()
|
||||||
opts.Frames = frames
|
opts.Frames = frames
|
||||||
@@ -66,22 +96,28 @@ func main() {
|
|||||||
nvidiaSMI = queryNvidiaSMI()
|
nvidiaSMI = queryNvidiaSMI()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
report := smokeReport{Machine: machine, Findings: findings, H264: results, Capture: captureResults, NVENC: nvencResult, NVENCD3D11: nvencD3D11Results, Nvidia: nvidiaSMI}
|
||||||
|
if findingErr != nil {
|
||||||
|
report.FindingError = findingErr.Error()
|
||||||
|
}
|
||||||
|
if reportPath != "" {
|
||||||
|
if err := writeJSONReport(reportPath, report); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "write report failed: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
if jsonOut {
|
if jsonOut {
|
||||||
enc := json.NewEncoder(os.Stdout)
|
enc := json.NewEncoder(os.Stdout)
|
||||||
enc.SetIndent("", " ")
|
enc.SetIndent("", " ")
|
||||||
payload := struct {
|
if err := enc.Encode(report); err != nil {
|
||||||
H264 []capture.H264SmokeResult `json:"h264"`
|
|
||||||
Capture []capture.CaptureSmokeResult `json:"capture,omitempty"`
|
|
||||||
NVENC *capture.NVENCSmokeResult `json:"nvenc,omitempty"`
|
|
||||||
NVENCD3D11 []capture.NVENCD3D11SmokeResult `json:"nvenc_d3d11,omitempty"`
|
|
||||||
Nvidia []string `json:"nvidia_smi,omitempty"`
|
|
||||||
}{H264: results, Capture: captureResults, NVENC: nvencResult, NVENCD3D11: nvencD3D11Results, Nvidia: nvidiaSMI}
|
|
||||||
if err := enc.Encode(payload); err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "json encode failed: %v\n", err)
|
fmt.Fprintf(os.Stderr, "json encode failed: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
printMachine(machine)
|
||||||
|
printMFTFindings(findings, findingErr)
|
||||||
|
fmt.Println()
|
||||||
printTable(results)
|
printTable(results)
|
||||||
if includeCapture {
|
if includeCapture {
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
@@ -102,6 +138,51 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func writeJSONReport(path string, report smokeReport) error {
|
||||||
|
f, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
enc := json.NewEncoder(f)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
return enc.Encode(report)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printMachine(info machineInfo) {
|
||||||
|
fmt.Println("Machine:")
|
||||||
|
fmt.Printf(" Host: %s\n OS: %s (%s)\n CPU: %s\n GPU: %s\n RAM: %s\n Time: %s\n", info.Hostname, info.OS, info.Arch, info.CPU, info.GPU, info.RAM, info.GeneratedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func printMFTFindings(findings []capture.H264MFTFinding, err error) {
|
||||||
|
fmt.Println("Media Foundation hardware H.264 findings:")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf(" ERROR: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(findings) == 0 {
|
||||||
|
fmt.Println(" No hardware H.264 MFTs found.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, finding := range findings {
|
||||||
|
status := "ok"
|
||||||
|
if !finding.ActivationOK {
|
||||||
|
status = "failed"
|
||||||
|
}
|
||||||
|
mode := "sync"
|
||||||
|
if finding.Asynchronous {
|
||||||
|
mode = "async"
|
||||||
|
}
|
||||||
|
fmt.Printf(" [%d] %s | activation=%s | mode=%s | d3d11=%t\n", finding.Index, finding.Name, status, mode, finding.D3D11Aware)
|
||||||
|
if finding.HardwareURL != "" {
|
||||||
|
fmt.Printf(" device: %s\n", finding.HardwareURL)
|
||||||
|
}
|
||||||
|
if finding.Error != "" {
|
||||||
|
fmt.Printf(" error: %s\n", finding.Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func parseResolutions(raw string) []capture.H264SmokeResolution {
|
func parseResolutions(raw string) []capture.H264SmokeResolution {
|
||||||
parts := parseStrings(raw)
|
parts := parseStrings(raw)
|
||||||
out := make([]capture.H264SmokeResolution, 0, len(parts))
|
out := make([]capture.H264SmokeResolution, 0, len(parts))
|
||||||
@@ -143,8 +224,8 @@ func parseStrings(raw string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func printTable(results []capture.H264SmokeResult) {
|
func printTable(results []capture.H264SmokeResult) {
|
||||||
fmt.Printf("%-18s %-5s %9s %7s %7s %8s %9s %8s %7s %9s %9s %s\n",
|
fmt.Printf("%-28s %-5s %-5s %9s %7s %7s %8s %9s %8s %7s %9s %9s %s\n",
|
||||||
"provider", "input", "size", "reqfps", "cfgfps", "config", "first_ms", "avg_ms", "frames", "avg_bytes", "total", "error")
|
"provider", "mode", "input", "size", "reqfps", "cfgfps", "config", "first_ms", "avg_ms", "frames", "avg_bytes", "total", "error")
|
||||||
for _, r := range results {
|
for _, r := range results {
|
||||||
status := "fail"
|
status := "fail"
|
||||||
if r.ConfigureOK && r.EncodeOK {
|
if r.ConfigureOK && r.EncodeOK {
|
||||||
@@ -152,8 +233,12 @@ func printTable(results []capture.H264SmokeResult) {
|
|||||||
} else if r.ConfigureOK {
|
} else if r.ConfigureOK {
|
||||||
status = "no-data"
|
status = "no-data"
|
||||||
}
|
}
|
||||||
fmt.Printf("%-18s %-5s %4dx%-4d %7d %7d %8s %9.2f %8.2f %7d %9.0f %9d %s\n",
|
mode := "sync"
|
||||||
r.Provider, r.Input, r.Width, r.Height, r.RequestedFPS, r.ConfiguredFPS, status,
|
if r.Asynchronous {
|
||||||
|
mode = "async"
|
||||||
|
}
|
||||||
|
fmt.Printf("%-28s %-5s %-5s %4dx%-4d %7d %7d %8s %9.2f %8.2f %7d %9.0f %9d %s\n",
|
||||||
|
r.Provider, mode, r.Input, r.Width, r.Height, r.RequestedFPS, r.ConfiguredFPS, status,
|
||||||
r.FirstOutputMS, r.AvgEncodeMS, r.FramesTried, r.AvgBytes, r.TotalBytes, r.Error)
|
r.FirstOutputMS, r.AvgEncodeMS, r.FramesTried, r.AvgBytes, r.TotalBytes, r.Error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user