mirror of
https://github.com/pion/mediadevices.git
synced 2026-04-22 15:57:27 +08:00
Add darwin runtime device observer support (#670)
* 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
This commit is contained in:
@@ -0,0 +1,70 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pion/mediadevices/pkg/driver"
|
||||||
|
"github.com/pion/mediadevices/pkg/driver/camera"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
fmt.Println("This example demonstrates query-based camera device discovery on Darwin.")
|
||||||
|
fmt.Println("The background observer automatically updates the manager's device list")
|
||||||
|
fmt.Println("when cameras are connected or disconnected.")
|
||||||
|
|
||||||
|
// Calling StartObserver without calling SetupObserver prior implicitly calls SetupObserver
|
||||||
|
// due to state machine internals. We make SetupObserver and StartObserver distinct because
|
||||||
|
// not all downstream programs will want to start pumping the NSRunLoop to handle events immediately.
|
||||||
|
err := camera.StartObserver()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("failed to start observer: %v\n", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
err := camera.DestroyObserver()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("failed to destroy observer: %v\n", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(os.Stdin)
|
||||||
|
queryCount := 0
|
||||||
|
|
||||||
|
queryDevices(0)
|
||||||
|
|
||||||
|
for {
|
||||||
|
fmt.Print("\nPress Enter to query (or 'q' to exit): ")
|
||||||
|
|
||||||
|
if !scanner.Scan() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
input := strings.TrimSpace(scanner.Text())
|
||||||
|
if strings.ToLower(input) == "q" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
queryCount++
|
||||||
|
queryDevices(queryCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryDevices(count int) {
|
||||||
|
if count > 0 {
|
||||||
|
fmt.Printf("Query #%d\n", count)
|
||||||
|
}
|
||||||
|
|
||||||
|
devices := driver.GetManager().Query(driver.FilterVideoRecorder())
|
||||||
|
|
||||||
|
if len(devices) == 0 {
|
||||||
|
fmt.Println("No video devices found.")
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Found %d video device(s):\n", len(devices))
|
||||||
|
for i, d := range devices {
|
||||||
|
info := d.Info()
|
||||||
|
fmt.Printf(" %d. %s [%s]\n", i+1, info.Name, info.Label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,6 +30,12 @@
|
|||||||
#import "AVFoundationBind.h"
|
#import "AVFoundationBind.h"
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
|
// AVFoundationBind.m is the entry point for cgo compilation (included by avfoundation_darwin.go).
|
||||||
|
// Including DeviceObserver.m here compiles both into a single compilation unit,
|
||||||
|
// making all symbols available to the linker.
|
||||||
|
#include "DeviceObserver.m"
|
||||||
|
|
||||||
|
|
||||||
#define CHK(condition, status) \
|
#define CHK(condition, status) \
|
||||||
do { \
|
do { \
|
||||||
if(!(condition)) { \
|
if(!(condition)) { \
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
// MIT License
|
||||||
|
//
|
||||||
|
// Copyright (c) 2019-2020 Pion
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
// of this software and associated documentation files (the "Software"), to deal
|
||||||
|
// in the Software without restriction, including without limitation the rights
|
||||||
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
// copies of the Software, and to permit persons to whom the Software is
|
||||||
|
// furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in all
|
||||||
|
// copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
// SOFTWARE.
|
||||||
|
|
||||||
|
#ifndef DEVICEOBSERVER_H
|
||||||
|
#define DEVICEOBSERVER_H
|
||||||
|
|
||||||
|
#include "AVFoundationBind.h"
|
||||||
|
|
||||||
|
typedef const char* STATUS;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char uid[MAX_DEVICE_UID_CHARS + 1]; // +1 for null terminator
|
||||||
|
char name[MAX_DEVICE_NAME_CHARS + 1];
|
||||||
|
} DeviceInfo;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
DeviceEventConnected = 0,
|
||||||
|
DeviceEventDisconnected = 1
|
||||||
|
} DeviceEventType;
|
||||||
|
|
||||||
|
// Callback function type for device events
|
||||||
|
// userData: user-provided context pointer
|
||||||
|
// eventType: connected or disconnected
|
||||||
|
// device: device info
|
||||||
|
typedef void (*DeviceEventCallback)(void *userData, DeviceEventType eventType, DeviceInfo *device);
|
||||||
|
|
||||||
|
// Initialize the device observer with a callback
|
||||||
|
// Returns NULL on success, error string on failure
|
||||||
|
STATUS DeviceObserverInit(DeviceEventCallback callback, void *userData);
|
||||||
|
|
||||||
|
// Start observing device events (notifications will be delivered via the run loop)
|
||||||
|
STATUS DeviceObserverStart(void);
|
||||||
|
|
||||||
|
// Stop observing device events
|
||||||
|
STATUS DeviceObserverStop(void);
|
||||||
|
|
||||||
|
// Cleanup the device observer
|
||||||
|
STATUS DeviceObserverDestroy(void);
|
||||||
|
|
||||||
|
// Get current list of video devices
|
||||||
|
// devices: output array (must have space for MAX_DEVICES)
|
||||||
|
// count: output count of devices found
|
||||||
|
STATUS DeviceObserverGetDevices(DeviceInfo *devices, int *count);
|
||||||
|
|
||||||
|
// Run the run loop for a specified duration (in seconds)
|
||||||
|
// This allows the observer to receive notifications
|
||||||
|
STATUS DeviceObserverRunFor(double seconds);
|
||||||
|
|
||||||
|
#endif // DEVICEOBSERVER_H
|
||||||
@@ -0,0 +1,260 @@
|
|||||||
|
// MIT License
|
||||||
|
//
|
||||||
|
// Copyright (c) 2019-2020 Pion
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
// of this software and associated documentation files (the "Software"), to deal
|
||||||
|
// in the Software without restriction, including without limitation the rights
|
||||||
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
// copies of the Software, and to permit persons to whom the Software is
|
||||||
|
// furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in all
|
||||||
|
// copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
// SOFTWARE.
|
||||||
|
|
||||||
|
// Naming Convention (let "name" as an actual variable name):
|
||||||
|
// - mName: "name" is a member of an Objective C object
|
||||||
|
// - pName: "name" is a C pointer
|
||||||
|
// - refName: "name" is an Objective C object reference
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import <AVFoundation/AVFoundation.h>
|
||||||
|
#import <string.h>
|
||||||
|
#import "DeviceObserver.h"
|
||||||
|
|
||||||
|
|
||||||
|
extern void goDeviceEventCallback(void *pUserData, int eventType, DeviceInfo *pDevice);
|
||||||
|
|
||||||
|
void deviceEventBridge(void *pUserData, DeviceEventType eventType, DeviceInfo *pDevice) {
|
||||||
|
goDeviceEventCallback(pUserData, (int)eventType, pDevice);
|
||||||
|
}
|
||||||
|
|
||||||
|
@interface DeviceObserverDelegate : NSObject {
|
||||||
|
DeviceEventCallback mCallback;
|
||||||
|
void *mUserData;
|
||||||
|
AVCaptureDeviceDiscoverySession *mDiscoverySession;
|
||||||
|
BOOL mObserving;
|
||||||
|
}
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation DeviceObserverDelegate
|
||||||
|
|
||||||
|
- (instancetype)initWithCallback:(DeviceEventCallback)callback userData:(void *)pUserData {
|
||||||
|
self = [super init];
|
||||||
|
if (self) {
|
||||||
|
mCallback = callback;
|
||||||
|
mUserData = pUserData;
|
||||||
|
mObserving = NO;
|
||||||
|
|
||||||
|
NSArray *refDeviceTypes = @[
|
||||||
|
AVCaptureDeviceTypeBuiltInWideAngleCamera,
|
||||||
|
AVCaptureDeviceTypeExternal
|
||||||
|
];
|
||||||
|
|
||||||
|
mDiscoverySession = [[AVCaptureDeviceDiscoverySession
|
||||||
|
discoverySessionWithDeviceTypes:refDeviceTypes
|
||||||
|
mediaType:AVMediaTypeVideo
|
||||||
|
position:AVCaptureDevicePositionUnspecified] retain];
|
||||||
|
}
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)startObserving {
|
||||||
|
if (mObserving) return;
|
||||||
|
|
||||||
|
[mDiscoverySession addObserver:self
|
||||||
|
forKeyPath:@"devices"
|
||||||
|
options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew)
|
||||||
|
context:nil];
|
||||||
|
|
||||||
|
mObserving = YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)stopObserving {
|
||||||
|
if (!mObserving) return;
|
||||||
|
|
||||||
|
[mDiscoverySession removeObserver:self forKeyPath:@"devices"];
|
||||||
|
mObserving = NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)observeValueForKeyPath:(NSString *)keyPath
|
||||||
|
ofObject:(id)object
|
||||||
|
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
|
||||||
|
context:(void *)pContext {
|
||||||
|
|
||||||
|
if (![keyPath isEqualToString:@"devices"]) return;
|
||||||
|
|
||||||
|
NSArray<AVCaptureDevice *> *refOldDevices = change[NSKeyValueChangeOldKey];
|
||||||
|
NSArray<AVCaptureDevice *> *refNewDevices = change[NSKeyValueChangeNewKey];
|
||||||
|
|
||||||
|
if ([refOldDevices isKindOfClass:[NSNull class]]) refOldDevices = @[];
|
||||||
|
if ([refNewDevices isKindOfClass:[NSNull class]]) refNewDevices = @[];
|
||||||
|
|
||||||
|
// Build sets of device UIDs for comparison
|
||||||
|
NSMutableSet *refOldUIDs = [NSMutableSet set];
|
||||||
|
NSMutableDictionary *refOldDeviceMap = [NSMutableDictionary dictionary];
|
||||||
|
for (AVCaptureDevice *refDevice in refOldDevices) {
|
||||||
|
[refOldUIDs addObject:refDevice.uniqueID];
|
||||||
|
refOldDeviceMap[refDevice.uniqueID] = refDevice;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSMutableSet *refNewUIDs = [NSMutableSet set];
|
||||||
|
NSMutableDictionary *refNewDeviceMap = [NSMutableDictionary dictionary];
|
||||||
|
for (AVCaptureDevice *refDevice in refNewDevices) {
|
||||||
|
[refNewUIDs addObject:refDevice.uniqueID];
|
||||||
|
refNewDeviceMap[refDevice.uniqueID] = refDevice;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find added devices
|
||||||
|
NSMutableSet *refAddedUIDs = [refNewUIDs mutableCopy];
|
||||||
|
[refAddedUIDs minusSet:refOldUIDs];
|
||||||
|
|
||||||
|
// Find removed devices
|
||||||
|
NSMutableSet *refRemovedUIDs = [refOldUIDs mutableCopy];
|
||||||
|
[refRemovedUIDs minusSet:refNewUIDs];
|
||||||
|
|
||||||
|
// Notify about added devices
|
||||||
|
for (NSString *uid in refAddedUIDs) {
|
||||||
|
AVCaptureDevice *refDevice = refNewDeviceMap[uid];
|
||||||
|
DeviceInfo info;
|
||||||
|
memset(&info, 0, sizeof(info));
|
||||||
|
strlcpy(info.uid, refDevice.uniqueID.UTF8String, sizeof(info.uid));
|
||||||
|
strlcpy(info.name, refDevice.localizedName.UTF8String, sizeof(info.name));
|
||||||
|
|
||||||
|
if (mCallback) {
|
||||||
|
mCallback(mUserData, DeviceEventConnected, &info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify about removed devices
|
||||||
|
for (NSString *uid in refRemovedUIDs) {
|
||||||
|
AVCaptureDevice *refDevice = refOldDeviceMap[uid];
|
||||||
|
DeviceInfo info;
|
||||||
|
memset(&info, 0, sizeof(info));
|
||||||
|
strlcpy(info.uid, refDevice.uniqueID.UTF8String, sizeof(info.uid));
|
||||||
|
strlcpy(info.name, refDevice.localizedName.UTF8String, sizeof(info.name));
|
||||||
|
|
||||||
|
if (mCallback) {
|
||||||
|
mCallback(mUserData, DeviceEventDisconnected, &info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[refAddedUIDs release];
|
||||||
|
[refRemovedUIDs release];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)dealloc {
|
||||||
|
[self stopObserving];
|
||||||
|
[mDiscoverySession release];
|
||||||
|
[super dealloc];
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
// Global observer instance
|
||||||
|
static DeviceObserverDelegate *refObserver = nil;
|
||||||
|
|
||||||
|
STATUS DeviceObserverInit(DeviceEventCallback callback, void *pUserData) {
|
||||||
|
@autoreleasepool {
|
||||||
|
if (refObserver != nil) {
|
||||||
|
return "observer already initialized";
|
||||||
|
}
|
||||||
|
|
||||||
|
refObserver = [[DeviceObserverDelegate alloc] initWithCallback:callback userData:pUserData];
|
||||||
|
if (refObserver == nil) {
|
||||||
|
return "failed to create observer";
|
||||||
|
}
|
||||||
|
|
||||||
|
return STATUS_OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
STATUS DeviceObserverStart(void) {
|
||||||
|
@autoreleasepool {
|
||||||
|
if (refObserver == nil) {
|
||||||
|
return "observer not initialized";
|
||||||
|
}
|
||||||
|
|
||||||
|
[refObserver startObserving];
|
||||||
|
return STATUS_OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
STATUS DeviceObserverStop(void) {
|
||||||
|
@autoreleasepool {
|
||||||
|
if (refObserver == nil) {
|
||||||
|
return "observer not initialized";
|
||||||
|
}
|
||||||
|
|
||||||
|
[refObserver stopObserving];
|
||||||
|
return STATUS_OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
STATUS DeviceObserverDestroy(void) {
|
||||||
|
@autoreleasepool {
|
||||||
|
if (refObserver == nil) {
|
||||||
|
return "observer not initialized";
|
||||||
|
}
|
||||||
|
|
||||||
|
[refObserver stopObserving];
|
||||||
|
[refObserver release];
|
||||||
|
refObserver = nil;
|
||||||
|
|
||||||
|
return STATUS_OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
STATUS DeviceObserverGetDevices(DeviceInfo *pDevices, int *pCount) {
|
||||||
|
@autoreleasepool {
|
||||||
|
if (pDevices == NULL || pCount == NULL) {
|
||||||
|
return "invalid arguments";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use discovery session for device enumeration
|
||||||
|
NSArray *refDeviceTypes = @[
|
||||||
|
AVCaptureDeviceTypeBuiltInWideAngleCamera,
|
||||||
|
AVCaptureDeviceTypeExternal
|
||||||
|
];
|
||||||
|
|
||||||
|
AVCaptureDeviceDiscoverySession *refSession = [AVCaptureDeviceDiscoverySession
|
||||||
|
discoverySessionWithDeviceTypes:refDeviceTypes
|
||||||
|
mediaType:AVMediaTypeVideo
|
||||||
|
position:AVCaptureDevicePositionUnspecified];
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
for (AVCaptureDevice *refDevice in refSession.devices) {
|
||||||
|
if (i >= MAX_DEVICES) break;
|
||||||
|
|
||||||
|
memset(&pDevices[i], 0, sizeof(DeviceInfo));
|
||||||
|
strlcpy(pDevices[i].uid, refDevice.uniqueID.UTF8String, sizeof(pDevices[i].uid));
|
||||||
|
strlcpy(pDevices[i].name, refDevice.localizedName.UTF8String, sizeof(pDevices[i].name));
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
*pCount = i;
|
||||||
|
return STATUS_OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
STATUS DeviceObserverRunFor(double seconds) {
|
||||||
|
@autoreleasepool {
|
||||||
|
// Add a timer to keep the run loop alive
|
||||||
|
NSTimer *refTimer = [NSTimer scheduledTimerWithTimeInterval:seconds
|
||||||
|
target:[NSDate class]
|
||||||
|
selector:@selector(date)
|
||||||
|
userInfo:nil
|
||||||
|
repeats:NO];
|
||||||
|
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:seconds]];
|
||||||
|
[refTimer invalidate];
|
||||||
|
return STATUS_OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,386 @@
|
|||||||
|
package avfoundation
|
||||||
|
|
||||||
|
/*
|
||||||
|
#cgo CFLAGS: -x objective-c
|
||||||
|
#cgo LDFLAGS: -framework AVFoundation -framework Foundation -framework CoreMedia -framework CoreVideo
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include "AVFoundationBind/DeviceObserver.h"
|
||||||
|
|
||||||
|
extern void deviceEventBridge(void *userData, DeviceEventType eventType, DeviceInfo *device);
|
||||||
|
|
||||||
|
static const char* DeviceObserverInitWithBridge() {
|
||||||
|
return DeviceObserverInit(deviceEventBridge, NULL);
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
type observerStateType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
observerInitial observerStateType = iota
|
||||||
|
observerSetup // KVO initialized on main thread but not pumping run loop
|
||||||
|
observerStarting // Starting run loop (transitioning to running)
|
||||||
|
observerRunning // Run loop is actively pumping
|
||||||
|
observerDestroyed // Destroyed and cannot be restarted
|
||||||
|
)
|
||||||
|
|
||||||
|
// deviceObserver manages the AVFoundation device observer lifecycle with the singleton pattern.
|
||||||
|
// The observer is single-use. Once DestroyObserver is called, it cannot be restarted.
|
||||||
|
type deviceObserver struct {
|
||||||
|
// Signals observer to transition to the startup state
|
||||||
|
signalStart chan struct{}
|
||||||
|
// Signals observer to destroy and stop pumping the NSRunLoop in the bg routine (if running)
|
||||||
|
signalDestroy chan struct{}
|
||||||
|
// Closed when setup state logic completes.
|
||||||
|
setupDone chan struct{}
|
||||||
|
// Closed when startup state logic completes.
|
||||||
|
startDone chan struct{}
|
||||||
|
// Coordinates waiting for the observer goroutine to complete
|
||||||
|
wg sync.WaitGroup
|
||||||
|
|
||||||
|
// mu protects all below state fields.
|
||||||
|
// Must not be held when invoking user callbacks to avoid deadlock (double lock acquisition).
|
||||||
|
mu sync.Mutex
|
||||||
|
deviceCache map[string]Device
|
||||||
|
state observerStateType
|
||||||
|
onDeviceChange func(Device, DeviceEventType)
|
||||||
|
setupErr error
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
observerSingleton *deviceObserver
|
||||||
|
observerSingletonOnce sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
func getObserver() *deviceObserver {
|
||||||
|
observerSingletonOnce.Do(func() {
|
||||||
|
observerSingleton = &deviceObserver{
|
||||||
|
deviceCache: make(map[string]Device),
|
||||||
|
state: observerInitial,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return observerSingleton
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeviceEventType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
DeviceEventConnected DeviceEventType = C.DeviceEventConnected
|
||||||
|
DeviceEventDisconnected DeviceEventType = C.DeviceEventDisconnected
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetOnDeviceChange(f func(Device, DeviceEventType)) {
|
||||||
|
obs := getObserver()
|
||||||
|
obs.mu.Lock()
|
||||||
|
defer obs.mu.Unlock()
|
||||||
|
obs.onDeviceChange = f
|
||||||
|
}
|
||||||
|
|
||||||
|
func createDevice(uid, name string) Device {
|
||||||
|
var d Device
|
||||||
|
d.UID = uid
|
||||||
|
d.Name = name
|
||||||
|
|
||||||
|
// Copy strings to C char arrays
|
||||||
|
cUID := C.CString(uid)
|
||||||
|
defer C.free(unsafe.Pointer(cUID))
|
||||||
|
C.strncpy(&d.cDevice.uid[0], cUID, C.MAX_DEVICE_UID_CHARS)
|
||||||
|
d.cDevice.uid[C.MAX_DEVICE_UID_CHARS] = 0
|
||||||
|
|
||||||
|
cName := C.CString(name)
|
||||||
|
defer C.free(unsafe.Pointer(cName))
|
||||||
|
C.strncpy(&d.cDevice.name[0], cName, C.MAX_DEVICE_NAME_CHARS)
|
||||||
|
d.cDevice.name[C.MAX_DEVICE_NAME_CHARS] = 0
|
||||||
|
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
//export goDeviceEventCallback
|
||||||
|
func goDeviceEventCallback(userData unsafe.Pointer, eventType C.int, device *C.DeviceInfo) {
|
||||||
|
uid := C.GoString(&device.uid[0])
|
||||||
|
name := C.GoString(&device.name[0])
|
||||||
|
|
||||||
|
d := createDevice(uid, name)
|
||||||
|
et := DeviceEventType(eventType)
|
||||||
|
|
||||||
|
obs := getObserver()
|
||||||
|
obs.mu.Lock()
|
||||||
|
if eventType == C.DeviceEventConnected {
|
||||||
|
obs.deviceCache[uid] = d
|
||||||
|
} else if eventType == C.DeviceEventDisconnected {
|
||||||
|
delete(obs.deviceCache, uid)
|
||||||
|
}
|
||||||
|
cb := obs.onDeviceChange
|
||||||
|
obs.mu.Unlock()
|
||||||
|
|
||||||
|
if cb != nil {
|
||||||
|
cb(d, et)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setup initializes the device observer and starts a goroutine locked to a thread for NSRunLoop,
|
||||||
|
// but does not begin pumping the run loop yet. The goroutine waits idle until start is called.
|
||||||
|
// This function assumes the caller invoked it from the main thread to set up AVFoundation KVO properly.
|
||||||
|
func (obs *deviceObserver) setup() error {
|
||||||
|
obs.mu.Lock()
|
||||||
|
|
||||||
|
switch obs.state {
|
||||||
|
case observerInitial:
|
||||||
|
// Continue with setup
|
||||||
|
case observerSetup, observerStarting, observerRunning:
|
||||||
|
// Already setup or beyond
|
||||||
|
obs.mu.Unlock()
|
||||||
|
return nil
|
||||||
|
case observerDestroyed:
|
||||||
|
obs.mu.Unlock()
|
||||||
|
return fmt.Errorf("device observer is single-use and was destroyed, so it cannot be restarted")
|
||||||
|
}
|
||||||
|
|
||||||
|
if obs.setupDone != nil {
|
||||||
|
done := obs.setupDone
|
||||||
|
obs.mu.Unlock()
|
||||||
|
<-done
|
||||||
|
obs.mu.Lock()
|
||||||
|
err := obs.setupErr
|
||||||
|
obs.mu.Unlock()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're first to setup, initialize the channels
|
||||||
|
obs.signalStart = make(chan struct{})
|
||||||
|
obs.signalDestroy = make(chan struct{})
|
||||||
|
obs.setupDone = make(chan struct{})
|
||||||
|
obs.startDone = make(chan struct{})
|
||||||
|
obs.setupErr = nil
|
||||||
|
obs.wg.Add(1)
|
||||||
|
obs.mu.Unlock()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer obs.wg.Done()
|
||||||
|
runtime.LockOSThread()
|
||||||
|
defer runtime.UnlockOSThread()
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if status := C.DeviceObserverInitWithBridge(); status != nil {
|
||||||
|
err = fmt.Errorf("failed to init observer: %s", C.GoString(status))
|
||||||
|
} else if status := C.DeviceObserverStart(); status != nil {
|
||||||
|
C.DeviceObserverDestroy() // remember to clean up C objects on error
|
||||||
|
err = fmt.Errorf("failed to start observer: %s", C.GoString(status))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
obs.mu.Lock()
|
||||||
|
obs.state = observerInitial
|
||||||
|
obs.setupErr = err
|
||||||
|
obs.mu.Unlock()
|
||||||
|
close(obs.setupDone)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate device cache and prepare initial device list for callbacks
|
||||||
|
var devices [C.MAX_DEVICES]C.DeviceInfo
|
||||||
|
var count C.int
|
||||||
|
status := C.DeviceObserverGetDevices(&devices[0], &count)
|
||||||
|
var initialDevices []Device
|
||||||
|
obs.mu.Lock()
|
||||||
|
if status == nil {
|
||||||
|
obs.deviceCache = make(map[string]Device)
|
||||||
|
for i := 0; i < int(count); i++ {
|
||||||
|
uid := C.GoString(&devices[i].uid[0])
|
||||||
|
name := C.GoString(&devices[i].name[0])
|
||||||
|
dev := createDevice(uid, name)
|
||||||
|
obs.deviceCache[uid] = dev
|
||||||
|
initialDevices = append(initialDevices, dev)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
obs.state = observerSetup
|
||||||
|
obs.mu.Unlock()
|
||||||
|
|
||||||
|
close(obs.setupDone)
|
||||||
|
|
||||||
|
// STATE BOUNDARY: setup phase complete, now entering startup phase
|
||||||
|
obs.waitForStartAndRun(initialDevices)
|
||||||
|
}()
|
||||||
|
|
||||||
|
<-obs.setupDone // waits for goroutine to complete setup
|
||||||
|
obs.mu.Lock()
|
||||||
|
err := obs.setupErr
|
||||||
|
obs.mu.Unlock()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// waitForStartAndRun waits for the start signal, then transitions to running state
|
||||||
|
// and pumps the NSRunLoop.
|
||||||
|
func (obs *deviceObserver) waitForStartAndRun(initialDevices []Device) {
|
||||||
|
// Wait for signal to start pumping or destroy
|
||||||
|
select {
|
||||||
|
case <-obs.signalDestroy:
|
||||||
|
C.DeviceObserverStop()
|
||||||
|
C.DeviceObserverDestroy()
|
||||||
|
close(obs.startDone)
|
||||||
|
return
|
||||||
|
case <-obs.signalStart:
|
||||||
|
// Transition to running
|
||||||
|
}
|
||||||
|
|
||||||
|
obs.mu.Lock()
|
||||||
|
cb := obs.onDeviceChange
|
||||||
|
obs.state = observerRunning
|
||||||
|
obs.mu.Unlock()
|
||||||
|
|
||||||
|
close(obs.startDone)
|
||||||
|
|
||||||
|
// Replay current devices
|
||||||
|
if cb != nil {
|
||||||
|
for _, dev := range initialDevices {
|
||||||
|
cb(dev, DeviceEventConnected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// STATE BOUNDARY: startup -> running
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-obs.signalDestroy:
|
||||||
|
// STATE BOUNDARY: running -> destroyed
|
||||||
|
C.DeviceObserverStop()
|
||||||
|
C.DeviceObserverDestroy()
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
C.DeviceObserverRunFor(0.1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// start signals the observer goroutine to begin pumping the run loop.
|
||||||
|
func (obs *deviceObserver) start() error {
|
||||||
|
obs.mu.Lock()
|
||||||
|
|
||||||
|
for {
|
||||||
|
switch obs.state {
|
||||||
|
case observerInitial:
|
||||||
|
// Need to setup first
|
||||||
|
obs.mu.Unlock()
|
||||||
|
if err := obs.setup(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
obs.mu.Lock()
|
||||||
|
continue // re-check state as it may have changed by another goroutine e.g. destroyed
|
||||||
|
case observerStarting:
|
||||||
|
// Another goroutine is starting the run loop; wait on same result
|
||||||
|
done := obs.startDone
|
||||||
|
obs.mu.Unlock()
|
||||||
|
<-done
|
||||||
|
return nil
|
||||||
|
case observerRunning:
|
||||||
|
obs.mu.Unlock()
|
||||||
|
return nil
|
||||||
|
case observerDestroyed:
|
||||||
|
obs.mu.Unlock()
|
||||||
|
return fmt.Errorf("cannot start observer: observer has been destroyed and cannot be restarted")
|
||||||
|
case observerSetup:
|
||||||
|
// Proceed to signal start
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
obs.state = observerStarting
|
||||||
|
pump := obs.signalStart
|
||||||
|
obs.mu.Unlock()
|
||||||
|
|
||||||
|
close(pump)
|
||||||
|
|
||||||
|
<-obs.startDone
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// destroy destroys the device observer and releases all C/Objective-C resources.
|
||||||
|
// The observer cannot be restarted after being destroyed.
|
||||||
|
func (obs *deviceObserver) destroy() error {
|
||||||
|
obs.mu.Lock()
|
||||||
|
|
||||||
|
for {
|
||||||
|
switch obs.state {
|
||||||
|
case observerInitial:
|
||||||
|
obs.state = observerDestroyed
|
||||||
|
destroy := obs.signalDestroy
|
||||||
|
obs.mu.Unlock()
|
||||||
|
if destroy != nil { // may be nil if setup wasn't called
|
||||||
|
close(destroy)
|
||||||
|
obs.wg.Wait()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case observerDestroyed:
|
||||||
|
obs.mu.Unlock()
|
||||||
|
return nil
|
||||||
|
case observerSetup, observerRunning:
|
||||||
|
// Set state to destroyed before unlocking to prevent concurrent destroy
|
||||||
|
obs.state = observerDestroyed
|
||||||
|
case observerStarting:
|
||||||
|
// Wait for transition to running
|
||||||
|
done := obs.startDone
|
||||||
|
obs.mu.Unlock()
|
||||||
|
<-done
|
||||||
|
obs.mu.Lock() // lock and check state again
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy := obs.signalDestroy
|
||||||
|
obs.mu.Unlock()
|
||||||
|
|
||||||
|
close(destroy)
|
||||||
|
obs.wg.Wait()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupObserver initializes the device observer and starts a goroutine
|
||||||
|
// locked to a thread for NSRunLoop, but does not begin pumping the run loop yet.
|
||||||
|
// The goroutine waits idle until StartObserver is called, avoiding CPU overhead.
|
||||||
|
// Safe to call concurrently and idempotently.
|
||||||
|
func SetupObserver() error {
|
||||||
|
return getObserver().setup()
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartObserver signals the observer goroutine to begin pumping the run loop.
|
||||||
|
// If SetupObserver has not been called, StartObserver will call it first.
|
||||||
|
// Safe to call concurrently and idempotently.
|
||||||
|
func StartObserver() error {
|
||||||
|
return getObserver().start()
|
||||||
|
}
|
||||||
|
|
||||||
|
// DestroyObserver destroys the device observer and releases all C/Objective-C resources.
|
||||||
|
// The observer is single-use and cannot be restarted after being destroyed.
|
||||||
|
// Safe to call concurrently and idempotently.
|
||||||
|
func DestroyObserver() error {
|
||||||
|
return getObserver().destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
// LookupCachedDevice returns the cached device that matches the provided UID.
|
||||||
|
// The returned boolean indicates whether the device was present in the cache.
|
||||||
|
// Callers should verify IsObserverRunning before relying on the result.
|
||||||
|
func LookupCachedDevice(uid string) (Device, bool) {
|
||||||
|
obs := getObserver()
|
||||||
|
obs.mu.Lock()
|
||||||
|
defer obs.mu.Unlock()
|
||||||
|
|
||||||
|
dev, ok := obs.deviceCache[uid]
|
||||||
|
return dev, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsObserverRunning reports whether the device observer has successfully started
|
||||||
|
// and populated the in-memory cache.
|
||||||
|
func IsObserverRunning() bool {
|
||||||
|
obs := getObserver()
|
||||||
|
obs.mu.Lock()
|
||||||
|
defer obs.mu.Unlock()
|
||||||
|
return obs.state == observerRunning
|
||||||
|
}
|
||||||
@@ -0,0 +1,297 @@
|
|||||||
|
//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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,9 +5,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrUnimplemented = NewError("not implemented")
|
ErrObserverUnavailable = NewError("observer unavailable (not started or destroyed)")
|
||||||
ErrBusy = NewError("device or resource busy")
|
ErrUnimplemented = NewError("not implemented")
|
||||||
ErrNoDevice = NewError("no such device")
|
ErrBusy = NewError("device or resource busy")
|
||||||
|
ErrNoDevice = NewError("no such device")
|
||||||
)
|
)
|
||||||
|
|
||||||
type errorString struct {
|
type errorString struct {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/pion/mediadevices/pkg/avfoundation"
|
"github.com/pion/mediadevices/pkg/avfoundation"
|
||||||
"github.com/pion/mediadevices/pkg/driver"
|
"github.com/pion/mediadevices/pkg/driver"
|
||||||
|
"github.com/pion/mediadevices/pkg/driver/availability"
|
||||||
"github.com/pion/mediadevices/pkg/frame"
|
"github.com/pion/mediadevices/pkg/frame"
|
||||||
"github.com/pion/mediadevices/pkg/io/video"
|
"github.com/pion/mediadevices/pkg/io/video"
|
||||||
"github.com/pion/mediadevices/pkg/prop"
|
"github.com/pion/mediadevices/pkg/prop"
|
||||||
@@ -44,6 +45,72 @@ func Initialize() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetupObserver initializes the device observer on the main thread without starting monitoring.
|
||||||
|
// This allows setup on the main thread (required by macOS) without CPU overhead until StartObserver is called.
|
||||||
|
// The caller must invoke SetupObserver from the main thread for proper NSRunLoop setup.
|
||||||
|
// Safe to call concurrently and idempotent; multiple calls are no-ops if already setup.
|
||||||
|
func SetupObserver() error {
|
||||||
|
manager := driver.GetManager()
|
||||||
|
|
||||||
|
avfoundation.SetOnDeviceChange(func(device avfoundation.Device, event avfoundation.DeviceEventType) {
|
||||||
|
switch event {
|
||||||
|
case avfoundation.DeviceEventConnected:
|
||||||
|
drivers := manager.Query(func(d driver.Driver) bool {
|
||||||
|
return d.Info().Label == device.UID
|
||||||
|
})
|
||||||
|
if len(drivers) > 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cam := newCamera(device)
|
||||||
|
manager.Register(cam, driver.Info{
|
||||||
|
Label: device.UID,
|
||||||
|
DeviceType: driver.Camera,
|
||||||
|
Name: device.Name,
|
||||||
|
})
|
||||||
|
|
||||||
|
case avfoundation.DeviceEventDisconnected:
|
||||||
|
drivers := manager.Query(func(d driver.Driver) bool {
|
||||||
|
return d.Info().Label == device.UID
|
||||||
|
})
|
||||||
|
for _, d := range drivers {
|
||||||
|
status := d.Status()
|
||||||
|
if status != driver.StateClosed {
|
||||||
|
if err := d.Close(); err != nil {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
manager.Delete(d.ID())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return avfoundation.SetupObserver()
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartObserver starts the background observer to monitor for device changes.
|
||||||
|
// If SetupObserver has not been called, StartObserver will call it first.
|
||||||
|
// Safe to call concurrently and idempotently.
|
||||||
|
func StartObserver() error {
|
||||||
|
// Call SetupObserver first to ensure SetOnDeviceChange callback is registered.
|
||||||
|
// This is safe as observer methods are idempotent and handle concurrency.
|
||||||
|
if err := SetupObserver(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := avfoundation.StartObserver(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return syncVideoRecorders(driver.GetManager())
|
||||||
|
}
|
||||||
|
|
||||||
|
// DestroyObserver destroys the device observer and releases all resources.
|
||||||
|
// The observer is single-use and cannot be restarted after being destroyed.
|
||||||
|
// Safe to call concurrently and idempotently.
|
||||||
|
func DestroyObserver() error {
|
||||||
|
return avfoundation.DestroyObserver()
|
||||||
|
}
|
||||||
|
|
||||||
func newCamera(device avfoundation.Device) *camera {
|
func newCamera(device avfoundation.Device) *camera {
|
||||||
return &camera{
|
return &camera{
|
||||||
device: device,
|
device: device,
|
||||||
@@ -57,15 +124,20 @@ func (cam *camera) Open() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (cam *camera) Close() error {
|
func (cam *camera) Close() error {
|
||||||
if cam.rcClose != nil {
|
|
||||||
cam.rcClose()
|
|
||||||
}
|
|
||||||
|
|
||||||
if cam.cancel != nil {
|
if cam.cancel != nil {
|
||||||
cam.cancel()
|
cam.cancel()
|
||||||
|
cam.cancel = nil
|
||||||
}
|
}
|
||||||
|
if cam.rcClose != nil {
|
||||||
return cam.session.Close()
|
cam.rcClose()
|
||||||
|
cam.rcClose = nil
|
||||||
|
}
|
||||||
|
if cam.session != nil {
|
||||||
|
err := cam.session.Close()
|
||||||
|
cam.session = nil
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cam *camera) VideoRecord(property prop.Media) (video.Reader, error) {
|
func (cam *camera) VideoRecord(property prop.Media) (video.Reader, error) {
|
||||||
@@ -106,3 +178,68 @@ func (cam *camera) VideoRecord(property prop.Media) (video.Reader, error) {
|
|||||||
func (cam *camera) Properties() []prop.Media {
|
func (cam *camera) Properties() []prop.Media {
|
||||||
return cam.session.Properties()
|
return cam.session.Properties()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cam *camera) IsAvailable() (bool, error) {
|
||||||
|
if !avfoundation.IsObserverRunning() {
|
||||||
|
return false, availability.ErrObserverUnavailable
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := avfoundation.LookupCachedDevice(cam.device.UID); !ok {
|
||||||
|
return false, availability.ErrNoDevice
|
||||||
|
}
|
||||||
|
|
||||||
|
// Probe device availability by attempting to open a session
|
||||||
|
session, err := avfoundation.NewSession(cam.device)
|
||||||
|
if err != nil {
|
||||||
|
return false, availability.ErrBusy
|
||||||
|
}
|
||||||
|
if session == nil {
|
||||||
|
panic("session was nil while error was nil")
|
||||||
|
}
|
||||||
|
session.Close()
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// syncVideoRecorders keeps the manager in lockstep with the hardware before the first user query.
|
||||||
|
func syncVideoRecorders(manager *driver.Manager) error {
|
||||||
|
devices, err := avfoundation.Devices(avfoundation.Video)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
current := make(map[string]struct{}, len(devices))
|
||||||
|
for _, device := range devices {
|
||||||
|
current[device.UID] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
registered := manager.Query(driver.FilterVideoRecorder())
|
||||||
|
registeredByLabel := make(map[string]struct{}, len(registered))
|
||||||
|
|
||||||
|
// drop any registered drivers whose UID isn't currently present
|
||||||
|
for _, d := range registered {
|
||||||
|
label := d.Info().Label
|
||||||
|
registeredByLabel[label] = struct{}{}
|
||||||
|
if _, ok := current[label]; !ok {
|
||||||
|
manager.Delete(d.ID())
|
||||||
|
delete(registeredByLabel, label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// register any new devices that appeared between the init() call and the observer start
|
||||||
|
for _, device := range devices {
|
||||||
|
if _, ok := registeredByLabel[device.UID]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
cam := newCamera(device)
|
||||||
|
manager.Register(cam, driver.Info{
|
||||||
|
Label: device.UID,
|
||||||
|
DeviceType: driver.Camera,
|
||||||
|
Name: device.Name,
|
||||||
|
})
|
||||||
|
registeredByLabel[device.UID] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
//go:build darwin
|
||||||
// +build darwin
|
// +build darwin
|
||||||
|
|
||||||
// $ go test -v . -tags darwin -run="^TestCameraFrameFormatSupport$"
|
// $ go test -v . -tags darwin -run="^TestCameraFrameFormatSupport$"
|
||||||
@@ -8,6 +9,8 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/pion/mediadevices/pkg/avfoundation"
|
"github.com/pion/mediadevices/pkg/avfoundation"
|
||||||
|
"github.com/pion/mediadevices/pkg/driver"
|
||||||
|
"github.com/pion/mediadevices/pkg/driver/availability"
|
||||||
"github.com/pion/mediadevices/pkg/frame"
|
"github.com/pion/mediadevices/pkg/frame"
|
||||||
"github.com/pion/mediadevices/pkg/prop"
|
"github.com/pion/mediadevices/pkg/prop"
|
||||||
)
|
)
|
||||||
@@ -61,3 +64,138 @@ func TestCameraFrameFormatSupport(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestCameraCloseIdempotency tests that Close can be called multiple times safely.
|
||||||
|
func TestCameraCloseIdempotency(t *testing.T) {
|
||||||
|
devices, err := avfoundation.Devices(avfoundation.Video)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(devices) == 0 {
|
||||||
|
t.Skip("No video devices available for testing")
|
||||||
|
}
|
||||||
|
|
||||||
|
cam := newCamera(devices[0])
|
||||||
|
if err := cam.Open(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close multiple times should not error
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
if err := cam.Close(); err != nil {
|
||||||
|
t.Errorf("Close call %d failed: %v", i+1, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify internal state was cleared
|
||||||
|
if cam.session != nil {
|
||||||
|
t.Error("Session should be nil after close")
|
||||||
|
}
|
||||||
|
if cam.rcClose != nil {
|
||||||
|
t.Error("rcClose should be nil after close")
|
||||||
|
}
|
||||||
|
if cam.cancel != nil {
|
||||||
|
t.Error("cancel should be nil after close")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestCameraIsAvailableObserverNotRunning tests IsAvailable when observer is not running.
|
||||||
|
func TestCameraIsAvailableObserverNotRunning(t *testing.T) {
|
||||||
|
devices, err := avfoundation.Devices(avfoundation.Video)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(devices) == 0 {
|
||||||
|
t.Skip("No video devices available for testing")
|
||||||
|
}
|
||||||
|
|
||||||
|
cam := newCamera(devices[0])
|
||||||
|
|
||||||
|
available, err := cam.IsAvailable()
|
||||||
|
if available {
|
||||||
|
t.Error("Camera should not be available when observer is not running")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != availability.ErrObserverUnavailable {
|
||||||
|
t.Errorf("Expected ErrObserverUnavailable, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNewCamera tests camera constructor.
|
||||||
|
func TestNewCamera(t *testing.T) {
|
||||||
|
testDevice := avfoundation.Device{
|
||||||
|
UID: "test-uid",
|
||||||
|
Name: "Test Camera",
|
||||||
|
}
|
||||||
|
|
||||||
|
cam := newCamera(testDevice)
|
||||||
|
|
||||||
|
if cam == nil {
|
||||||
|
t.Fatal("newCamera returned nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cam.device.UID != testDevice.UID {
|
||||||
|
t.Errorf("Expected device UID %q, got %q", testDevice.UID, cam.device.UID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cam.device.Name != testDevice.Name {
|
||||||
|
t.Errorf("Expected device name %q, got %q", testDevice.Name, cam.device.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSyncVideoRecorders tests the syncVideoRecorders function.
|
||||||
|
func TestSyncVideoRecorders(t *testing.T) {
|
||||||
|
manager := driver.GetManager()
|
||||||
|
|
||||||
|
// Initial state
|
||||||
|
initialDrivers := manager.Query(driver.FilterVideoRecorder())
|
||||||
|
initialCount := len(initialDrivers)
|
||||||
|
|
||||||
|
// Run sync
|
||||||
|
err := syncVideoRecorders(manager)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("syncVideoRecorders failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify drivers were synced
|
||||||
|
afterDrivers := manager.Query(driver.FilterVideoRecorder())
|
||||||
|
afterCount := len(afterDrivers)
|
||||||
|
|
||||||
|
// The count should match the actual devices available
|
||||||
|
devices, err := avfoundation.Devices(avfoundation.Video)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if afterCount != len(devices) {
|
||||||
|
t.Logf("Warning: Expected %d drivers after sync, got %d (initial: %d)",
|
||||||
|
len(devices), afterCount, initialCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestObserverFunctionsIdempotent tests that observer functions can be called multiple times.
|
||||||
|
func TestObserverFunctionsIdempotent(t *testing.T) {
|
||||||
|
// This test may have side effects on the global observer state
|
||||||
|
// In a real scenario, you'd want to reset the observer between tests
|
||||||
|
|
||||||
|
// SetupObserver should be idempotent
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
if err := SetupObserver(); err != nil {
|
||||||
|
t.Errorf("SetupObserver call %d failed: %v", i+1, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartObserver should be idempotent
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
if err := StartObserver(); err != nil {
|
||||||
|
t.Errorf("StartObserver call %d failed: %v", i+1, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
if err := DestroyObserver(); err != nil {
|
||||||
|
t.Errorf("DestroyObserver failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -98,6 +98,21 @@ func Initialize() {
|
|||||||
discover(discovered, "/dev/video*")
|
discover(discovered, "/dev/video*")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetupObserver is a stub implementation for Linux.
|
||||||
|
func SetupObserver() error {
|
||||||
|
return availability.ErrUnimplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartObserver is a stub implementation for Linux.
|
||||||
|
func StartObserver() error {
|
||||||
|
return availability.ErrUnimplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
// DestroyObserver is a stub implementation for Linux.
|
||||||
|
func DestroyObserver() error {
|
||||||
|
return availability.ErrUnimplemented
|
||||||
|
}
|
||||||
|
|
||||||
func discover(discovered map[string]struct{}, pattern string) {
|
func discover(discovered map[string]struct{}, pattern string) {
|
||||||
devices, err := filepath.Glob(pattern)
|
devices, err := filepath.Glob(pattern)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
//go:build linux || windows
|
||||||
|
|
||||||
|
package camera
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/pion/mediadevices/pkg/driver/availability"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestSetupObserver tests the stub implementation of SetupObserver.
|
||||||
|
func TestSetupObserver(t *testing.T) {
|
||||||
|
err := SetupObserver()
|
||||||
|
if !errors.Is(err, availability.ErrUnimplemented) {
|
||||||
|
t.Errorf("SetupObserver() should return ErrUnimplemented for stub implementation, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestStartObserver tests the stub implementation of StartObserver.
|
||||||
|
func TestStartObserver(t *testing.T) {
|
||||||
|
err := StartObserver()
|
||||||
|
if !errors.Is(err, availability.ErrUnimplemented) {
|
||||||
|
t.Errorf("StartObserver() should return ErrUnimplemented for stub implementation, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDestroyObserver tests the stub implementation of DestroyObserver.
|
||||||
|
func TestDestroyObserver(t *testing.T) {
|
||||||
|
err := DestroyObserver()
|
||||||
|
if !errors.Is(err, availability.ErrUnimplemented) {
|
||||||
|
t.Errorf("DestroyObserver() should return ErrUnimplemented for stub implementation, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestObserverFunctionsIdempotent tests that observer functions can be called multiple times safely.
|
||||||
|
func TestObserverFunctionsIdempotent(t *testing.T) {
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
if err := SetupObserver(); !errors.Is(err, availability.ErrUnimplemented) {
|
||||||
|
t.Errorf("SetupObserver() call %d should return ErrUnimplemented, got: %v", i+1, err)
|
||||||
|
}
|
||||||
|
if err := StartObserver(); !errors.Is(err, availability.ErrUnimplemented) {
|
||||||
|
t.Errorf("StartObserver() call %d should return ErrUnimplemented, got: %v", i+1, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
if err := DestroyObserver(); !errors.Is(err, availability.ErrUnimplemented) {
|
||||||
|
t.Errorf("DestroyObserver() call %d should return ErrUnimplemented, got: %v", i+1, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
"github.com/pion/mediadevices/pkg/driver"
|
"github.com/pion/mediadevices/pkg/driver"
|
||||||
|
"github.com/pion/mediadevices/pkg/driver/availability"
|
||||||
"github.com/pion/mediadevices/pkg/frame"
|
"github.com/pion/mediadevices/pkg/frame"
|
||||||
"github.com/pion/mediadevices/pkg/io/video"
|
"github.com/pion/mediadevices/pkg/io/video"
|
||||||
"github.com/pion/mediadevices/pkg/prop"
|
"github.com/pion/mediadevices/pkg/prop"
|
||||||
@@ -58,6 +59,21 @@ func Initialize() {
|
|||||||
C.freeCameraList(&list, &errStr)
|
C.freeCameraList(&list, &errStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetupObserver is a stub implementation for Windows.
|
||||||
|
func SetupObserver() error {
|
||||||
|
return availability.ErrUnimplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartObserver is a stub implementation for Windows.
|
||||||
|
func StartObserver() error {
|
||||||
|
return availability.ErrUnimplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
// DestroyObserver is a stub implementation for Windows.
|
||||||
|
func DestroyObserver() error {
|
||||||
|
return availability.ErrUnimplemented
|
||||||
|
}
|
||||||
|
|
||||||
func (c *camera) Open() error {
|
func (c *camera) Open() error {
|
||||||
c.ch = make(chan []byte)
|
c.ch = make(chan []byte)
|
||||||
c.cam = &C.camera{
|
c.cam = &C.camera{
|
||||||
|
|||||||
Reference in New Issue
Block a user