nativeaudio

audio playback for Go
Log | Files | Refs | README | LICENSE

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 }