Skip to content

Commit e7c7f89

Browse files
committed
Merge branch 'osx-support-clean' into develop
2 parents 227cca1 + 3b46204 commit e7c7f89

22 files changed

+2059
-30
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,3 +242,5 @@ test-templates/
242242

243243
*.bak
244244
*.orig
245+
246+
**/.DS_Store

Directory.Build.props

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
<RootNamespace>FlashCap</RootNamespace>
1919
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
2020
<GenerateDocumentationFile>true</GenerateDocumentationFile>
21-
<NoWarn>$(NoWarn);CS1570;CS1591;CA1416;CS8981</NoWarn>
21+
<NoWarn>$(NoWarn);CS1570;CS1591;CA1416;CS8981;NETSDK1215</NoWarn>
2222

2323
<Product>FlashCap</Product>
2424
<Trademark>FlashCap</Trademark>
@@ -50,7 +50,7 @@
5050
</PropertyGroup>
5151

5252
<ItemGroup>
53-
<PackageReference Include="RelaxVersioner" Version="3.3.0" PrivateAssets="All" />
53+
<PackageReference Include="RelaxVersioner" Version="3.13.0" PrivateAssets="All" />
5454
</ItemGroup>
5555

5656
<ItemGroup Condition="'$(Configuration)' == 'Release'">

FSharp.FlashCap/FSharp.FlashCap.fsproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<TargetFrameworks>net48;netstandard2.0;netstandard2.1;net5.0;net6.0;net7.0;net8.0</TargetFrameworks>

FSharp.FlashCap/PixelBufferExtension.fs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ namespace FlashCap
1111

1212
open System.Diagnostics
1313

14+
#nowarn 3261
15+
1416
[<AutoOpen>]
1517
module public PixelBufferExtension =
1618

FSharp.FlashCap/Utilities/UtilitiesExtension.fs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ open System
1313
open System.IO
1414
open System.Runtime.CompilerServices
1515

16+
#nowarn 3261
17+
1618
[<Extension>]
1719
type UtilitiesExtension =
1820

@@ -25,5 +27,5 @@ type UtilitiesExtension =
2527
[<Extension>]
2628
static member asStream(self: byte[]) =
2729
match self with
28-
| null -> new MemoryStream(ArrayEx.Empty<byte>())
30+
| null -> new MemoryStream(ArrayEx.Empty<byte>())
2931
| _ -> new MemoryStream(self)

FlashCap.Core/CaptureDeviceDescriptor.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public enum DeviceTypes
2222
VideoForWindows,
2323
DirectShow,
2424
V4L2,
25+
AVFoundation,
2526
}
2627

2728
public enum TranscodeFormats

FlashCap.Core/CaptureDevices.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
////////////////////////////////////////////////////////////////////////////
1+
////////////////////////////////////////////////////////////////////////////
22
//
33
// FlashCap - Independent camera capture library.
44
// Copyright (c) Kouji Matsui (@kozy_kekyo, @kekyo@mastodon.cloud)
5+
// Copyright (c) Yoh Deadfall (@YohDeadfall)
6+
// Copyright (c) Felipe Ferreira Quintella (@ffquintella)
57
//
68
// Licensed under Apache-v2: https://opensource.org/licenses/Apache-2.0
79
//
@@ -35,7 +37,9 @@ protected virtual IEnumerable<CaptureDeviceDescriptor> OnEnumerateDescriptors()
3537
new DirectShowDevices(this.DefaultBufferPool).OnEnumerateDescriptors().
3638
Concat(new VideoForWindowsDevices(this.DefaultBufferPool).OnEnumerateDescriptors()),
3739
NativeMethods.Platforms.Linux =>
38-
new V4L2Devices(this.DefaultBufferPool).OnEnumerateDescriptors(),
40+
new V4L2Devices().OnEnumerateDescriptors(),
41+
NativeMethods.Platforms.MacOS =>
42+
new AVFoundationDevices().OnEnumerateDescriptors(),
3943
_ =>
4044
ArrayEx.Empty<CaptureDeviceDescriptor>(),
4145
};
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
////////////////////////////////////////////////////////////////////////////
2+
//
3+
// FlashCap - Independent camera capture library.
4+
// Copyright (c) Yoh Deadfall (@YohDeadfall)
5+
// Copyright (c) Felipe Ferreira Quintella (@ffquintella)
6+
//
7+
// Licensed under Apache-v2: https://opensource.org/licenses/Apache-2.0
8+
//
9+
////////////////////////////////////////////////////////////////////////////
10+
11+
using System;
12+
using System.Diagnostics;
13+
using System.Linq;
14+
using System.Runtime.InteropServices;
15+
using System.Threading;
16+
using System.Threading.Tasks;
17+
using FlashCap.Internal;
18+
using static FlashCap.Internal.AVFoundation.LibAVFoundation;
19+
using static FlashCap.Internal.NativeMethods;
20+
using static FlashCap.Internal.NativeMethods_AVFoundation.LibCoreFoundation;
21+
using static FlashCap.Internal.NativeMethods_AVFoundation.LibCoreMedia;
22+
using static FlashCap.Internal.NativeMethods_AVFoundation.LibCoreVideo;
23+
24+
namespace FlashCap.Devices;
25+
26+
public sealed class AVFoundationDevice : CaptureDevice
27+
{
28+
private readonly string uniqueID;
29+
30+
private DispatchQueue? queue;
31+
private AVCaptureDevice? device;
32+
private AVCaptureDeviceInput? deviceInput;
33+
private AVCaptureVideoDataOutput? deviceOutput;
34+
private AVCaptureSession? session;
35+
private FrameProcessor? frameProcessor;
36+
private IntPtr bitmapHeader;
37+
38+
public AVFoundationDevice(string uniqueID, string modelID) :
39+
base(uniqueID, modelID)
40+
{
41+
this.uniqueID = uniqueID;
42+
}
43+
44+
protected override async Task OnDisposeAsync()
45+
{
46+
if (this.session != null)
47+
{
48+
this.session.StopRunning();
49+
this.session.Dispose();
50+
}
51+
52+
this.device?.Dispose();
53+
this.deviceInput?.Dispose();
54+
this.deviceOutput?.Dispose();
55+
this.queue?.Dispose();
56+
57+
Marshal.FreeHGlobal(this.bitmapHeader);
58+
59+
if (frameProcessor is not null)
60+
{
61+
await frameProcessor.DisposeAsync().ConfigureAwait(false);
62+
}
63+
64+
await base.OnDisposeAsync().ConfigureAwait(false);
65+
}
66+
67+
protected override Task OnInitializeAsync(VideoCharacteristics characteristics, TranscodeFormats transcodeFormat,
68+
FrameProcessor frameProcessor, CancellationToken ct)
69+
{
70+
71+
this.frameProcessor = frameProcessor;
72+
this.Characteristics = characteristics;
73+
74+
if (!NativeMethods_AVFoundation.PixelFormatMap.TryGetValue(characteristics.PixelFormat, out var pixelFormatType) ||
75+
!NativeMethods.GetCompressionAndBitCount(characteristics.PixelFormat, out var compression, out var bitCount))
76+
{
77+
throw new ArgumentException(
78+
$"FlashCap: Couldn't set video format: UniqueID={this.uniqueID}");
79+
}
80+
81+
this.bitmapHeader = NativeMethods.AllocateMemory(new IntPtr(MarshalEx.SizeOf<BITMAPINFOHEADER>()));
82+
83+
try
84+
{
85+
unsafe
86+
{
87+
var pBih = (BITMAPINFOHEADER*)this.bitmapHeader.ToPointer();
88+
89+
pBih->biSize = MarshalEx.SizeOf<BITMAPINFOHEADER>();
90+
pBih->biCompression = compression;
91+
pBih->biPlanes = 1;
92+
pBih->biBitCount = bitCount;
93+
pBih->biWidth = characteristics.Width;
94+
pBih->biHeight = characteristics.Height;
95+
pBih->biSizeImage = pBih->CalculateImageSize();
96+
}
97+
98+
this.queue = new DispatchQueue(nameof(FlashCap));
99+
this.device = AVCaptureDevice.DeviceWithUniqueID(uniqueID)
100+
?? throw new InvalidOperationException(
101+
$"FlashCap: Couldn't find device: UniqueID={this.uniqueID}");
102+
103+
this.device.LockForConfiguration();
104+
try
105+
{
106+
var formatSelected = this.device.Formats
107+
.FirstOrDefault(format =>
108+
format.FormatDescription.Dimensions is var dimensions &&
109+
format.FormatDescription.MediaType == CMMediaType.Video &&
110+
dimensions.Width == characteristics.Width &&
111+
dimensions.Height == characteristics.Height)
112+
?? throw new InvalidOperationException(
113+
$"FlashCap: Couldn't set video format: UniqueID={this.uniqueID}");
114+
115+
this.device.ActiveFormat = formatSelected;
116+
117+
var frameDuration = CMTimeMake(
118+
characteristics.FramesPerSecond.Denominator,
119+
characteristics.FramesPerSecond.Numerator);
120+
121+
device.ActiveVideoMinFrameDuration = frameDuration;
122+
device.ActiveVideoMaxFrameDuration = frameDuration;
123+
124+
this.deviceInput = new AVCaptureDeviceInput(device);
125+
126+
this.deviceOutput = new AVCaptureVideoDataOutput();
127+
128+
if (this.deviceOutput.AvailableVideoCVPixelFormatTypes?.Any() == true)
129+
{
130+
var validPixelFormat = this.deviceOutput.AvailableVideoCVPixelFormatTypes.FirstOrDefault(p => p == pixelFormatType);
131+
this.deviceOutput.SetPixelFormatType(validPixelFormat);
132+
}
133+
else
134+
{
135+
// Fallback to the mapped pixel format if no available list is provided
136+
this.deviceOutput.SetPixelFormatType(pixelFormatType);
137+
}
138+
139+
this.deviceOutput.SetSampleBufferDelegate(new VideoBufferHandler(this), this.queue);
140+
this.deviceOutput.AlwaysDiscardsLateVideoFrames = true;
141+
}
142+
finally
143+
{
144+
this.device.UnlockForConfiguration();
145+
}
146+
147+
this.session = new AVCaptureSession();
148+
this.session.AddInput(this.deviceInput);
149+
150+
if (this.session.CanAddOutput(deviceOutput))
151+
{
152+
this.session.AddOutput(this.deviceOutput);
153+
}
154+
else
155+
{
156+
throw new Exception("Can't add video output");
157+
}
158+
159+
return TaskCompat.CompletedTask;
160+
}
161+
catch
162+
{
163+
NativeMethods.FreeMemory(this.bitmapHeader);
164+
throw;
165+
}
166+
}
167+
168+
protected override Task OnStartAsync(CancellationToken ct)
169+
{
170+
this.session?.StartRunning();
171+
return TaskCompat.CompletedTask;
172+
}
173+
174+
protected override Task OnStopAsync(CancellationToken ct)
175+
{
176+
this.session?.StopRunning();
177+
return TaskCompat.CompletedTask;
178+
}
179+
180+
protected override void OnCapture(IntPtr pData, int size, long timestampMicroseconds, long frameIndex, PixelBuffer buffer)
181+
{
182+
buffer.CopyIn(this.bitmapHeader, pData, size, timestampMicroseconds, frameIndex, TranscodeFormats.Auto);
183+
}
184+
185+
internal sealed class VideoBufferHandler : AVCaptureVideoDataOutputSampleBuffer
186+
{
187+
private readonly AVFoundationDevice device;
188+
private int frameIndex;
189+
190+
public VideoBufferHandler(AVFoundationDevice device)
191+
{
192+
this.device = device;
193+
}
194+
195+
public override void DidDropSampleBuffer(IntPtr captureOutput, IntPtr sampleBuffer, IntPtr connection)
196+
{
197+
Debug.WriteLine("Dropped");
198+
var valid = CMSampleBufferIsValid(sampleBuffer);
199+
}
200+
201+
public void CaptureOutputCallback(IntPtr self, IntPtr _cmd, IntPtr output, IntPtr sampleBuffer,
202+
IntPtr connection)
203+
{
204+
205+
var pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
206+
207+
if (pixelBuffer == IntPtr.Zero)
208+
{
209+
throw new Exception("Failed to get image buffer");
210+
}
211+
else
212+
{
213+
// Lock the base address for reading, process the buffer, etc.
214+
CVPixelBufferLockBaseAddress(pixelBuffer, PixelBufferLockFlags.ReadOnly);
215+
216+
try
217+
{
218+
// Process the pixel buffer as needed
219+
this.device.frameProcessor?.OnFrameArrived(
220+
this.device,
221+
CVPixelBufferGetBaseAddress(pixelBuffer),
222+
(int)CVPixelBufferGetDataSize(pixelBuffer),
223+
(long)(CMTimeGetSeconds(CMSampleBufferGetDecodeTimeStamp(sampleBuffer)) * 1000),
224+
frameIndex++);
225+
}
226+
finally
227+
{
228+
CVPixelBufferUnlockBaseAddress(pixelBuffer, PixelBufferLockFlags.ReadOnly);
229+
}
230+
}
231+
}
232+
}
233+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
////////////////////////////////////////////////////////////////////////////
2+
//
3+
// FlashCap - Independent camera capture library.
4+
// Copyright (c) Yoh Deadfall (@YohDeadfall)
5+
// Copyright (c) Felipe Ferreira Quintella (@ffquintella)
6+
//
7+
// Licensed under Apache-v2: https://opensource.org/licenses/Apache-2.0
8+
//
9+
////////////////////////////////////////////////////////////////////////////
10+
11+
using System.Threading;
12+
using System.Threading.Tasks;
13+
14+
namespace FlashCap.Devices;
15+
16+
public sealed class AVFoundationDeviceDescriptor : CaptureDeviceDescriptor
17+
{
18+
private readonly string uniqueId;
19+
20+
internal AVFoundationDeviceDescriptor(
21+
string uniqueId, string modelId, string localizedName,
22+
VideoCharacteristics[] characteristics,
23+
BufferPool defaultBufferPool) :
24+
base(modelId, localizedName, characteristics, defaultBufferPool) =>
25+
this.uniqueId = uniqueId;
26+
27+
public override object Identity =>
28+
this.uniqueId;
29+
30+
public override DeviceTypes DeviceType =>
31+
DeviceTypes.AVFoundation;
32+
33+
protected override Task<CaptureDevice> OnOpenWithFrameProcessorAsync(VideoCharacteristics characteristics,
34+
TranscodeFormats transcodeFormat,
35+
FrameProcessor frameProcessor, CancellationToken ct) =>
36+
this.InternalOnOpenWithFrameProcessorAsync(
37+
new AVFoundationDevice(this.uniqueId, this.Name),
38+
characteristics, transcodeFormat, frameProcessor, ct);
39+
}
40+
41+
42+

0 commit comments

Comments
 (0)