mirror of
https://github.com/pion/mediadevices.git
synced 2026-04-22 15:57:27 +08:00
03900dcb1b
* wip * wip * Organize * Remove unnecessary change in camera_darwin.go filtering * wip * Make observer stop safe during startup * wip IsAvailable impl * Fix non-darwin builds * Lock bg loop to main thread and add comment * Remove fmt prints * Simplify isAvailable; Add timeout for Read darwin * Match comment with code * Change to singleton pattern; Add clearer safer state machine states; Change language from Stop to Destroy; Add new error for when observer is unavailable; * Add stubs for linux * Move cancel() up so its not dead code sometimes * Add stubs for Windows too * Remove StopObserver usage * Add camera tests * Add device observer tests * Fix multiple destroy calls bug; Call setup in start * Improve isAvailable * Improve string handling in device observer c * Add error handling in example * Add comment about setup vs start * Rename and organize device observer darwin * Explicitly case initial state for setup * Fix potential destroy goroutine leak; Use only modern build tag; Return err not nil for stubs; Improve comments * Close startDone channel on device observer stop not wait
298 lines
6.7 KiB
Go
298 lines
6.7 KiB
Go
//go:build darwin
|
|
// +build darwin
|
|
|
|
package avfoundation
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
// TestGetObserverSingleton tests that getObserver returns the same instance.
|
|
func TestGetObserverSingleton(t *testing.T) {
|
|
obs1 := getObserver()
|
|
obs2 := getObserver()
|
|
|
|
if obs1 != obs2 {
|
|
t.Error("getObserver() should return the same singleton instance")
|
|
}
|
|
|
|
if obs1.deviceCache == nil {
|
|
t.Error("Observer device cache should be initialized")
|
|
}
|
|
|
|
if obs1.state != observerInitial {
|
|
t.Errorf("Initial observer state should be observerInitial, got: %v", obs1.state)
|
|
}
|
|
}
|
|
|
|
// TestCreateDevice tests device creation with UID and name.
|
|
func TestCreateDevice(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
uid string
|
|
devName string
|
|
}{
|
|
{
|
|
name: "simple device",
|
|
uid: "test-uid-123",
|
|
devName: "Test Camera",
|
|
},
|
|
{
|
|
name: "device with special characters",
|
|
uid: "camera_0x1234567890abcdef",
|
|
devName: "FaceTime HD Camera",
|
|
},
|
|
{
|
|
name: "empty strings",
|
|
uid: "",
|
|
devName: "",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
device := createDevice(tc.uid, tc.devName)
|
|
|
|
if device.UID != tc.uid {
|
|
t.Errorf("Expected UID %q, got %q", tc.uid, device.UID)
|
|
}
|
|
|
|
if device.Name != tc.devName {
|
|
t.Errorf("Expected Name %q, got %q", tc.devName, device.Name)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestSetOnDeviceChange tests setting and retrieving the device change callback.
|
|
func TestSetOnDeviceChange(t *testing.T) {
|
|
// Reset observer state for clean test (hacky)
|
|
// In production, the observer is a singleton
|
|
obs := getObserver()
|
|
obs.mu.Lock()
|
|
originalCallback := obs.onDeviceChange
|
|
obs.mu.Unlock()
|
|
|
|
// Restore original callback at end of test
|
|
defer func() {
|
|
obs.mu.Lock()
|
|
obs.onDeviceChange = originalCallback
|
|
obs.mu.Unlock()
|
|
}()
|
|
|
|
called := false
|
|
var capturedDevice Device
|
|
var capturedEvent DeviceEventType
|
|
|
|
SetOnDeviceChange(func(d Device, e DeviceEventType) {
|
|
called = true
|
|
capturedDevice = d
|
|
capturedEvent = e
|
|
})
|
|
|
|
// Verify callback was set
|
|
obs.mu.Lock()
|
|
if obs.onDeviceChange == nil {
|
|
t.Fatal("OnDeviceChange callback was not set")
|
|
}
|
|
|
|
// Manually trigger callback for testing
|
|
testDevice := createDevice("test-uid", "test-name")
|
|
testEvent := DeviceEventConnected
|
|
cb := obs.onDeviceChange
|
|
obs.mu.Unlock()
|
|
|
|
if cb != nil {
|
|
cb(testDevice, testEvent)
|
|
}
|
|
|
|
if !called {
|
|
t.Error("Callback was not invoked")
|
|
}
|
|
|
|
if capturedDevice.UID != "test-uid" {
|
|
t.Errorf("Expected captured UID %q, got %q", "test-uid", capturedDevice.UID)
|
|
}
|
|
|
|
if capturedEvent != DeviceEventConnected {
|
|
t.Errorf("Expected event %v, got %v", DeviceEventConnected, capturedEvent)
|
|
}
|
|
}
|
|
|
|
// TestLookupCachedDevice tests device cache lookups.
|
|
func TestLookupCachedDevice(t *testing.T) {
|
|
obs := getObserver()
|
|
|
|
// Add a test device to cache
|
|
testUID := "lookup-test-uid"
|
|
testDevice := createDevice(testUID, "Lookup Test Camera")
|
|
|
|
obs.mu.Lock()
|
|
obs.deviceCache[testUID] = testDevice
|
|
obs.mu.Unlock()
|
|
|
|
// Test successful lookup
|
|
device, ok := LookupCachedDevice(testUID)
|
|
if !ok {
|
|
t.Error("Expected to find device in cache")
|
|
}
|
|
|
|
if device.UID != testUID {
|
|
t.Errorf("Expected UID %q, got %q", testUID, device.UID)
|
|
}
|
|
|
|
// Test failed lookup
|
|
_, ok = LookupCachedDevice("non-existent-uid")
|
|
if ok {
|
|
t.Error("Expected not to find non-existent device in cache")
|
|
}
|
|
|
|
// Cleanup
|
|
obs.mu.Lock()
|
|
delete(obs.deviceCache, testUID)
|
|
obs.mu.Unlock()
|
|
}
|
|
|
|
// TestIsObserverRunning tests the observer running state check.
|
|
func TestIsObserverRunning(t *testing.T) {
|
|
obs := getObserver()
|
|
|
|
// Initially should not be running
|
|
obs.mu.Lock()
|
|
originalState := obs.state
|
|
obs.state = observerInitial
|
|
obs.mu.Unlock()
|
|
|
|
// Restore original state at end
|
|
defer func() {
|
|
obs.mu.Lock()
|
|
obs.state = originalState
|
|
obs.mu.Unlock()
|
|
}()
|
|
|
|
if IsObserverRunning() {
|
|
t.Error("Observer should not be running in initial state")
|
|
}
|
|
|
|
// Set state to running
|
|
obs.mu.Lock()
|
|
obs.state = observerRunning
|
|
obs.mu.Unlock()
|
|
|
|
if !IsObserverRunning() {
|
|
t.Error("Observer should be running after state set to observerRunning")
|
|
}
|
|
|
|
// Set state to other states
|
|
for _, state := range []observerStateType{observerSetup, observerStarting, observerDestroyed} {
|
|
obs.mu.Lock()
|
|
obs.state = state
|
|
obs.mu.Unlock()
|
|
|
|
if IsObserverRunning() {
|
|
t.Errorf("Observer should not be running in state %v", state)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestGoDeviceEventCallback tests the C-to-Go device event callback.
|
|
func TestGoDeviceEventCallback(t *testing.T) {
|
|
obs := getObserver()
|
|
|
|
// Clear device cache for clean test
|
|
obs.mu.Lock()
|
|
obs.deviceCache = make(map[string]Device)
|
|
originalCallback := obs.onDeviceChange
|
|
obs.mu.Unlock()
|
|
|
|
defer func() {
|
|
obs.mu.Lock()
|
|
obs.onDeviceChange = originalCallback
|
|
obs.deviceCache = make(map[string]Device)
|
|
obs.mu.Unlock()
|
|
}()
|
|
|
|
// Set up test callback
|
|
var callbackInvoked bool
|
|
var capturedDevice Device
|
|
var capturedEvent DeviceEventType
|
|
|
|
SetOnDeviceChange(func(d Device, e DeviceEventType) {
|
|
callbackInvoked = true
|
|
capturedDevice = d
|
|
capturedEvent = e
|
|
})
|
|
|
|
// Note: We cannot directly call goDeviceEventCallback with C types in a Go test
|
|
// without CGO setup. Instead, we test the logic that would be executed.
|
|
|
|
// Simulate connect event
|
|
testUID := "callback-test-uid"
|
|
testDevice := createDevice(testUID, "Callback Test Camera")
|
|
|
|
obs.mu.Lock()
|
|
obs.deviceCache[testUID] = testDevice
|
|
cb := obs.onDeviceChange
|
|
obs.mu.Unlock()
|
|
|
|
if cb != nil {
|
|
cb(testDevice, DeviceEventConnected)
|
|
}
|
|
|
|
if !callbackInvoked {
|
|
t.Error("User callback should have been invoked")
|
|
}
|
|
|
|
if capturedEvent != DeviceEventConnected {
|
|
t.Errorf("Expected DeviceEventConnected, got %v", capturedEvent)
|
|
}
|
|
|
|
// Verify device was added to cache
|
|
obs.mu.Lock()
|
|
_, exists := obs.deviceCache[testUID]
|
|
obs.mu.Unlock()
|
|
|
|
if !exists {
|
|
t.Error("Device should be in cache after connect event")
|
|
}
|
|
|
|
// Simulate disconnect event
|
|
callbackInvoked = false
|
|
obs.mu.Lock()
|
|
delete(obs.deviceCache, testUID)
|
|
cb = obs.onDeviceChange
|
|
obs.mu.Unlock()
|
|
|
|
if cb != nil {
|
|
cb(testDevice, DeviceEventDisconnected)
|
|
}
|
|
|
|
if !callbackInvoked {
|
|
t.Error("User callback should have been invoked for disconnect")
|
|
}
|
|
|
|
if capturedEvent != DeviceEventDisconnected {
|
|
t.Errorf("Expected DeviceEventDisconnected, got %v", capturedEvent)
|
|
}
|
|
|
|
if capturedDevice.UID != testUID {
|
|
t.Errorf("Expected captured device UID %q, got %q", testUID, capturedDevice.UID)
|
|
}
|
|
|
|
// Verify device was removed from cache
|
|
obs.mu.Lock()
|
|
_, exists = obs.deviceCache[testUID]
|
|
obs.mu.Unlock()
|
|
|
|
if exists {
|
|
t.Error("Device should not be in cache after disconnect event")
|
|
}
|
|
}
|
|
|
|
// TestDeviceEventTypes tests the device event type constants, verifying that they are different.
|
|
func TestDeviceEventTypes(t *testing.T) {
|
|
if DeviceEventConnected == DeviceEventDisconnected {
|
|
t.Error("DeviceEventConnected and DeviceEventDisconnected should be different")
|
|
}
|
|
}
|