callback.go (6661B)
1 //go:build windows 2 3 package mf 4 5 import ( 6 "errors" 7 "fmt" 8 "os" 9 "runtime" 10 "runtime/debug" 11 "sync/atomic" 12 "syscall" 13 "time" 14 "unsafe" 15 ) 16 17 // readTimeout bounds the wait for one asynchronous read. 18 // 19 // A healthy decode delivers samples in milliseconds. Media Foundation can 20 // block forever on a malformed stream, and in synchronous mode that hang 21 // is unrecoverable: the blocked call owns the calling thread and there is 22 // nothing to interrupt it. Driving the reader asynchronously makes the 23 // wait ours to abandon. 24 const readTimeout = 10 * time.Second 25 26 // errReadTimeout reports that Media Foundation never answered a read. 27 var errReadTimeout = errors.New("timed out waiting for the decoder") 28 29 // HRESULTs the callback needs beyond S_OK. 30 const ( 31 E_NOINTERFACE HRESULT = 0x80004002 32 E_POINTER HRESULT = 0x80004003 33 E_FAIL HRESULT = 0x80004005 34 ) 35 36 var ( 37 IID_IUnknown = GUID{0x00000000, 0x0000, 0x0000, [8]uint8{0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46}} 38 IID_IMFSourceReaderCallback = GUID{0xdeec8d99, 0xfa1d, 0x4d82, [8]uint8{0x84, 0xc2, 0x2c, 0x89, 0x69, 0x94, 0x48, 0x67}} 39 40 MF_SOURCE_READER_ASYNC_CALLBACK = GUID{0x1e3dbeac, 0xbb43, 0x4c35, [8]uint8{0xb5, 0x07, 0xcd, 0x64, 0x44, 0x64, 0xc9, 0x65}} 41 ) 42 43 // failed reports whether an HRESULT denotes failure, which is the sign bit 44 // of the 32-bit code. 45 func failed(hr HRESULT) bool { 46 return int32(uint32(hr)) < 0 47 } 48 49 // IUnknownVtbl is the head of every COM vtable. 50 type IUnknownVtbl struct { 51 QueryInterface uintptr 52 AddRef uintptr 53 Release uintptr 54 } 55 56 // sourceReaderCallbackVtbl is the vtable of IMFSourceReaderCallback. 57 type sourceReaderCallbackVtbl struct { 58 IUnknownVtbl 59 OnReadSample uintptr 60 OnFlush uintptr 61 OnEvent uintptr 62 } 63 64 // readResult is one delivery from OnReadSample. Its sample carries a 65 // reference taken by the callback, which the receiver must release. 66 type readResult struct { 67 status HRESULT 68 flags uint32 69 sample *IMFSample 70 } 71 72 // callback implements IMFSourceReaderCallback so the source reader can be 73 // driven asynchronously. 74 // 75 // A COM interface pointer points at a struct whose first word points to a 76 // vtable of function pointers. Handing Media Foundation the address of a 77 // Go struct whose first field is that vtable pointer therefore makes it a 78 // usable COM object, and each trampoline receives the struct back as its 79 // first argument. The vtable is a package-level singleton because 80 // [syscall.NewCallback] never frees what it creates. 81 // 82 // Media Foundation invokes the callback on its own worker thread, so 83 // deliveries arrive over a channel rather than being handled in place. 84 type callback struct { 85 vtbl *sourceReaderCallbackVtbl // must remain the first field. 86 87 refs atomic.Int32 88 pin runtime.Pinner 89 90 // ch holds at most one undelivered read. Reads are issued one at a 91 // time, so a deeper buffer would only hide a protocol mistake. 92 ch chan readResult 93 } 94 95 // newCallback returns a callback with one reference, pinned so Media 96 // Foundation may hold its address. 97 func newCallback() *callback { 98 c := &callback{vtbl: sourceReaderCallbackVtable, ch: make(chan readResult, 1)} 99 c.refs.Store(1) 100 c.pin.Pin(c) 101 return c 102 } 103 104 // AddRef implements IUnknown::AddRef. 105 func (c *callback) AddRef() uint32 { 106 return uint32(c.refs.Add(1)) 107 } 108 109 // Release implements IUnknown::Release, unpinning on the last reference. 110 // 111 // Media Foundation holds its own reference for as long as it might still 112 // call us, so the object outlives the stream that created it whenever a 113 // read is still outstanding. 114 func (c *callback) Release() uint32 { 115 n := c.refs.Add(-1) 116 if n == 0 { 117 c.drain() 118 c.pin.Unpin() 119 } 120 return uint32(n) 121 } 122 123 // QueryInterface implements IUnknown::QueryInterface. 124 func (c *callback) QueryInterface(riid *GUID, ppv *unsafe.Pointer) HRESULT { 125 if ppv == nil { 126 return E_POINTER 127 } 128 if riid == nil || (*riid != IID_IUnknown && *riid != IID_IMFSourceReaderCallback) { 129 *ppv = nil 130 return E_NOINTERFACE 131 } 132 *ppv = unsafe.Pointer(c) 133 c.AddRef() 134 return S_OK 135 } 136 137 // onReadSample takes delivery of one read. 138 // 139 // The sample is only guaranteed for the duration of this call, so it is 140 // retained before being handed over. When nothing is waiting, which is 141 // what happens once the reader has given up on a read, the sample is 142 // released here rather than leaked. 143 func (c *callback) onReadSample(status HRESULT, flags uint32, sample *IMFSample) HRESULT { 144 sample.AddRef() 145 select { 146 case c.ch <- readResult{status: status, flags: flags, sample: sample}: 147 default: 148 sample.Release() 149 } 150 return S_OK 151 } 152 153 // wait blocks for the next delivery, giving up after d. 154 func (c *callback) wait(d time.Duration) (readResult, error) { 155 timer := time.NewTimer(d) 156 defer timer.Stop() 157 select { 158 case r := <-c.ch: 159 return r, nil 160 case <-timer.C: 161 return readResult{}, errReadTimeout 162 } 163 } 164 165 // drain releases any sample left undelivered. 166 func (c *callback) drain() { 167 for { 168 select { 169 case r := <-c.ch: 170 r.sample.Release() 171 default: 172 return 173 } 174 } 175 } 176 177 // guard runs a COM method body and turns a panic into a failed HRESULT. 178 // 179 // A panic unwinding out of a [syscall.NewCallback] trampoline crosses C 180 // frames and takes the process down with it. COM callers expect failure 181 // as a status code, so report it as one. 182 func guard(method string, body func() HRESULT) (hr HRESULT) { 183 defer func() { 184 if p := recover(); p != nil { 185 fmt.Fprintf(os.Stderr, "nativeaudio: panic in %s: %v\n%s\n", method, p, debug.Stack()) 186 hr = E_FAIL 187 } 188 }() 189 return body() 190 } 191 192 // sourceReaderCallbackVtable is shared by every callback instance. The 193 // trampolines recover the instance from the pointer COM passes back. 194 var sourceReaderCallbackVtable = &sourceReaderCallbackVtbl{ 195 IUnknownVtbl: IUnknownVtbl{ 196 QueryInterface: syscall.NewCallback(func(this *callback, riid *GUID, ppv *unsafe.Pointer) uintptr { 197 return guard("IMFSourceReaderCallback::QueryInterface", func() HRESULT { return this.QueryInterface(riid, ppv) }) 198 }), 199 AddRef: syscall.NewCallback(func(this *callback) uintptr { 200 return guard("IMFSourceReaderCallback::AddRef", func() HRESULT { return HRESULT(this.AddRef()) }) 201 }), 202 Release: syscall.NewCallback(func(this *callback) uintptr { 203 return guard("IMFSourceReaderCallback::Release", func() HRESULT { return HRESULT(this.Release()) }) 204 }), 205 }, 206 OnReadSample: onReadSampleTrampoline, 207 // Nothing is flushed and no events are acted on, but both slots must 208 // be populated and must succeed. 209 OnFlush: syscall.NewCallback(func(this *callback, streamIndex uint32) uintptr { 210 return S_OK 211 }), 212 OnEvent: syscall.NewCallback(func(this *callback, streamIndex uint32, event uintptr) uintptr { 213 return S_OK 214 }), 215 }