Last active
July 31, 2022 20:15
-
-
Save DelphiWorlds/912a3020502ec4f283ae36cd36bc6113 to your computer and use it in GitHub Desktop.
Work in progress implementation of multipeer connectivity for iOS and macOS
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
unit DW.Multipeer.Mac; | |
interface | |
uses | |
System.TypInfo, | |
Macapi.ObjectiveC, | |
{$IF Defined(IOS)} | |
iOSapi.Foundation, | |
DW.iOSapi.Foundation, | |
DW.iOSapi.MultipeerConnectivity; | |
{$ELSE} | |
Macapi.Foundation, | |
DW.Macapi.AppKit, DW.Macapi.Foundation, DW.Macapi.MultipeerConnectivity; | |
{$ENDIF} | |
type | |
TPlatformMultipeer = class; | |
TMCAdvertiserAssistantDelegate = class(TOCLocal, MCAdvertiserAssistantDelegate) | |
private | |
FPlatformMultipeer: TPlatformMultipeer; | |
public | |
{ MCAdvertiserAssistantDelegate } | |
procedure advertiserAssistantDidDismissInvitation(advertiserAssistant: MCAdvertiserAssistant); cdecl; | |
procedure advertiserAssistantWillPresentInvitation(advertiserAssistant: MCAdvertiserAssistant); cdecl; | |
public | |
constructor Create(const APlatformMultipeer: TPlatformMultipeer); | |
end; | |
TMCBrowserViewControllerDelegate = class(TOCLocal, MCBrowserViewControllerDelegate) | |
private | |
FPlatformMultipeer: TPlatformMultipeer; | |
public | |
{ MCBrowserViewControllerDelegate } | |
function browserViewController(browserViewController: MCBrowserViewController; shouldPresentNearbyPeer: MCPeerID; | |
withDiscoveryInfo: NSDictionary): Boolean; cdecl; | |
procedure browserViewControllerDidFinish(browserViewController: MCBrowserViewController); cdecl; | |
procedure browserViewControllerWasCancelled(browserViewController: MCBrowserViewController); cdecl; | |
public | |
constructor Create(const APlatformMultipeer: TPlatformMultipeer); | |
end; | |
TMCNearbyServiceAdvertiserDelegate = class(TOCLocal, MCNearbyServiceAdvertiserDelegate) | |
private | |
FPlatformMultipeer: TPlatformMultipeer; | |
public | |
{ MCNearbyServiceAdvertiserDelegate } | |
procedure advertiser(advertiser: MCNearbyServiceAdvertiser; didReceiveInvitationFromPeer: MCPeerID; withContext: NSData; | |
invitationHandler: Pointer); overload; cdecl; | |
procedure advertiser(advertiser: MCNearbyServiceAdvertiser; didNotStartAdvertisingPeer: NSError); overload; cdecl; | |
public | |
constructor Create(const APlatformMultipeer: TPlatformMultipeer); | |
end; | |
TMCNearbyServiceBrowserDelegate = class(TOCLocal, MCNearbyServiceBrowserDelegate) | |
private | |
FPlatformMultipeer: TPlatformMultipeer; | |
public | |
{ MCNearbyServiceBrowserDelegate } | |
procedure browser(browser: MCNearbyServiceBrowser; didNotStartBrowsingForPeers: NSError); overload; cdecl; | |
procedure browser(browser: MCNearbyServiceBrowser; lostPeer: MCPeerID); overload; cdecl; | |
procedure browser(browser: MCNearbyServiceBrowser; foundPeer: MCPeerID; withDiscoveryInfo: NSDictionary); overload; cdecl; | |
public | |
constructor Create(const APlatformMultipeer: TPlatformMultipeer); | |
end; | |
TMCSessionDelegate = class(TOCLocal, MCSessionDelegate) | |
private | |
FPlatformMultipeer: TPlatformMultipeer; | |
public | |
{ MCSessionDelegate } | |
procedure session(session: MCSession; didStartReceivingResourceWithName: NSString; fromPeer: MCPeerID; withProgress: NSProgress); overload; cdecl; | |
procedure session(session: MCSession; didFinishReceivingResourceWithName: NSString; fromPeer: MCPeerID; atURL: NSURL; | |
withError: NSError); overload; cdecl; | |
procedure session(session: MCSession; didReceiveCertificate: NSArray; fromPeer: MCPeerID; | |
certificateHandler: Pointer); overload; cdecl; | |
procedure session(session: MCSession; peer: MCPeerID; didChangeState: MCSessionState); overload; cdecl; | |
procedure session(session: MCSession; didReceiveData: NSData; fromPeer: MCPeerID); overload; cdecl; | |
procedure session(session: MCSession; didReceiveStream: NSInputStream; withName: NSString; fromPeer: MCPeerID); overload; cdecl; | |
public | |
constructor Create(const APlatformMultipeer: TPlatformMultipeer); | |
end; | |
TConfirmPeerInviteResponse = reference to procedure(const Accepted: Boolean); | |
TConfirmPeerInviteEvent = procedure(Sender: TObject; const DisplayName: string; const Response: TConfirmPeerInviteResponse) of object; | |
TPlatformMultipeer = class(TObject) | |
private | |
FAdvertiser: MCNearbyServiceAdvertiser; | |
FAdvertiserDelegate: TMCNearbyServiceAdvertiserDelegate; | |
FAssistant: MCAdvertiserAssistant; | |
FAssistantDelegate: TMCAdvertiserAssistantDelegate; | |
FBrowser: MCNearbyServiceBrowser; | |
FBrowserDelegate: TMCNearbyServiceBrowserDelegate; | |
FBrowserViewController: MCBrowserViewController; | |
FBrowserViewControllerDelegate: TMCBrowserViewControllerDelegate; | |
FIsStarted: Boolean; | |
FIsUsingAssistant: Boolean; | |
FPeerID: MCPeerID; | |
FSession: MCSession; | |
FSessionDelegate: TMCSessionDelegate; | |
FServiceType: NSString; | |
FUseAssistant: Boolean; | |
{$IF not Defined(IOS)} | |
FViewController: NSViewController; | |
{$ENDIF} | |
FOnConfirmPeerInvite: TConfirmPeerInviteEvent; | |
procedure StartAdvertiser; | |
procedure StartAssistant; | |
protected | |
procedure AssistantDidDismissInvitation(const AAssistant: MCAdvertiserAssistant); | |
procedure AssistantWillPresentInvitation(const AAssistant: MCAdvertiserAssistant); | |
procedure BrowserDidNotStartBrowsingForPeers(const ABrowser: MCNearbyServiceBrowser; const AError: NSError); | |
procedure BrowserFoundPeer(const ABrowser: MCNearbyServiceBrowser; const APeer: MCPeerID; const ADiscoveryInfo: NSDictionary); | |
procedure BrowserLostPeer(const ABrowser: MCNearbyServiceBrowser; const APeer: MCPeerID); | |
procedure DismissBrowser; | |
procedure DoConfirmPeerInvite(const ADisplayName: string; const AResponse: TConfirmPeerInviteResponse); | |
procedure SessionDidChangeState(const ASession: MCSession; const APeer: MCPeerID; const AState: MCSessionState); | |
property Session: MCSession read FSession; | |
public | |
constructor Create(const AServiceType: string; const ADisplayName: string = ''); | |
destructor Destroy; override; | |
procedure ShowBrowser; | |
procedure Start; | |
procedure Stop; | |
property OnConfirmPeerInvite: TConfirmPeerInviteEvent read FOnConfirmPeerInvite write FOnConfirmPeerInvite; | |
end; | |
implementation | |
uses | |
System.SysUtils, | |
Macapi.CoreFoundation, Macapi.Helpers, | |
{$IF Defined(IOS)} | |
iOSapi.Helpers, | |
{$ELSE} | |
FMX.Platform.Mac, FMX.Forms, | |
{$ENDIF} | |
DW.Macapi.ObjCRuntime; | |
const | |
libFoundation = '/System/Library/Frameworks/Foundation.framework/Foundation'; | |
type | |
PNSString = Pointer; | |
TOSLog = record | |
public | |
class procedure d(const AFmt: string); overload; static; | |
class procedure d(const AFmt: string; const AParams: array of const); overload; static; | |
end; | |
procedure NSLog(format: PNSString); cdecl; varargs; external libFoundation name _PU + 'NSLog'; | |
function GetDeviceName: NSString; | |
begin | |
{$IF Defined(IOS)} | |
Result := TiOSHelper.CurrentDevice.name; | |
{$ELSE} | |
Result := TNSHost.Wrap(TNSHost.OCClass.currentHost).localizedName; | |
{$ENDIF} | |
end; | |
{ TOSLog } | |
class procedure TOSLog.d(const AFmt: string); | |
begin | |
d(AFmt, []); | |
end; | |
class procedure TOSLog.d(const AFmt: string; const AParams: array of const); | |
begin | |
NSLog(StringToID('DEBUG: ' + Format(AFmt, AParams))); | |
end; | |
{ TMCAdvertiserAssistantDelegate } | |
constructor TMCAdvertiserAssistantDelegate.Create(const APlatformMultipeer: TPlatformMultipeer); | |
begin | |
inherited Create; | |
FPlatformMultipeer := APlatformMultipeer; | |
end; | |
procedure TMCAdvertiserAssistantDelegate.advertiserAssistantDidDismissInvitation(advertiserAssistant: MCAdvertiserAssistant); | |
begin | |
TOSLog.d('didDismissInvitation'); | |
FPlatformMultipeer.AssistantDidDismissInvitation(advertiserAssistant); | |
end; | |
procedure TMCAdvertiserAssistantDelegate.advertiserAssistantWillPresentInvitation(advertiserAssistant: MCAdvertiserAssistant); | |
begin | |
TOSLog.d('willPresentInvitation'); | |
FPlatformMultipeer.AssistantWillPresentInvitation(advertiserAssistant); | |
end; | |
{ TMCBrowserViewControllerDelegate } | |
constructor TMCBrowserViewControllerDelegate.Create(const APlatformMultipeer: TPlatformMultipeer); | |
begin | |
inherited Create; | |
FPlatformMultipeer := APlatformMultipeer; | |
end; | |
function TMCBrowserViewControllerDelegate.browserViewController(browserViewController: MCBrowserViewController; shouldPresentNearbyPeer: MCPeerID; | |
withDiscoveryInfo: NSDictionary): Boolean; | |
begin | |
TOSLog.d('shouldPresentNearbyPeer'); | |
Result := True; //!!!! | |
end; | |
procedure TMCBrowserViewControllerDelegate.browserViewControllerDidFinish(browserViewController: MCBrowserViewController); | |
begin | |
TOSLog.d('browserViewControllerDidFinish'); | |
FPlatformMultipeer.DismissBrowser; | |
end; | |
procedure TMCBrowserViewControllerDelegate.browserViewControllerWasCancelled(browserViewController: MCBrowserViewController); | |
begin | |
TOSLog.d('browserViewControllerWasCancelled'); | |
FPlatformMultipeer.DismissBrowser; | |
end; | |
{ TMCNearbyServiceAdvertiserDelegate } | |
constructor TMCNearbyServiceAdvertiserDelegate.Create(const APlatformMultipeer: TPlatformMultipeer); | |
begin | |
inherited Create; | |
FPlatformMultipeer := APlatformMultipeer; | |
end; | |
procedure TMCNearbyServiceAdvertiserDelegate.advertiser(advertiser: MCNearbyServiceAdvertiser; didReceiveInvitationFromPeer: MCPeerID; | |
withContext: NSData; invitationHandler: Pointer); | |
var | |
LBlockImp: procedure(accept: Boolean; ignored: Pointer; session: Pointer); cdecl; | |
begin | |
TOSLog.d('didReceiveInvitationFromPeer'); | |
// MUST create the block imp BEFORE calling async method | |
@LBlockImp := imp_implementationWithBlock(invitationHandler); | |
FPlatformMultipeer.DoConfirmPeerInvite(NSStrToStr(didReceiveInvitationFromPeer.displayName), | |
procedure(const AAccepted: Boolean) | |
var | |
LSession: Pointer; | |
begin | |
if AAccepted then | |
LSession := NSObjectToID(FPlatformMultipeer.Session) | |
else | |
LSession := nil; | |
LBlockImp(AAccepted, nil, LSession); | |
imp_removeBlock(@LBlockImp); | |
end | |
); | |
end; | |
procedure TMCNearbyServiceAdvertiserDelegate.advertiser(advertiser: MCNearbyServiceAdvertiser; didNotStartAdvertisingPeer: NSError); | |
begin | |
TOSLog.d('didNotStartAdvertisingPeer'); | |
end; | |
{ TMCNearbyServiceBrowserDelegate } | |
constructor TMCNearbyServiceBrowserDelegate.Create(const APlatformMultipeer: TPlatformMultipeer); | |
begin | |
inherited Create; | |
FPlatformMultipeer := APlatformMultipeer; | |
end; | |
procedure TMCNearbyServiceBrowserDelegate.browser(browser: MCNearbyServiceBrowser; didNotStartBrowsingForPeers: NSError); | |
begin | |
TOSLog.d('didNotStartBrowsingForPeers'); | |
FPlatformMultipeer.BrowserDidNotStartBrowsingForPeers(browser, didNotStartBrowsingForPeers); | |
end; | |
procedure TMCNearbyServiceBrowserDelegate.browser(browser: MCNearbyServiceBrowser; lostPeer: MCPeerID); | |
begin | |
TOSLog.d('lostPeer'); | |
FPlatformMultipeer.BrowserLostPeer(browser, lostPeer); | |
end; | |
procedure TMCNearbyServiceBrowserDelegate.browser(browser: MCNearbyServiceBrowser; foundPeer: MCPeerID; withDiscoveryInfo: NSDictionary); | |
begin | |
TOSLog.d('foundPeer'); | |
FPlatformMultipeer.BrowserFoundPeer(browser, foundPeer, withDiscoveryInfo); | |
end; | |
{ TMCSessionDelegate } | |
constructor TMCSessionDelegate.Create(const APlatformMultipeer: TPlatformMultipeer); | |
begin | |
inherited Create; | |
FPlatformMultipeer := APlatformMultipeer; | |
end; | |
procedure TMCSessionDelegate.session(session: MCSession; didReceiveCertificate: NSArray; fromPeer: MCPeerID; certificateHandler: Pointer); | |
var | |
LBlockImp: procedure(accept: Boolean); cdecl; | |
begin | |
TOSLog.d('didReceiveCertificate'); | |
@LBlockImp := imp_implementationWithBlock(certificateHandler); | |
LBlockImp(True); | |
imp_removeBlock(@LBlockImp); | |
end; | |
procedure TMCSessionDelegate.session(session: MCSession; didFinishReceivingResourceWithName: NSString; fromPeer: MCPeerID; atURL: NSURL; | |
withError: NSError); | |
begin | |
TOSLog.d('didFinishReceivingResourceWithName'); | |
end; | |
procedure TMCSessionDelegate.session(session: MCSession; didStartReceivingResourceWithName: NSString; fromPeer: MCPeerID; withProgress: NSProgress); | |
begin | |
TOSLog.d('didStartReceivingResourceWithName'); | |
end; | |
procedure TMCSessionDelegate.session(session: MCSession; didReceiveStream: NSInputStream; withName: NSString; fromPeer: MCPeerID); | |
begin | |
TOSLog.d('didReceiveStream'); | |
end; | |
procedure TMCSessionDelegate.session(session: MCSession; didReceiveData: NSData; fromPeer: MCPeerID); | |
begin | |
TOSLog.d('didReceiveData'); | |
end; | |
procedure TMCSessionDelegate.session(session: MCSession; peer: MCPeerID; didChangeState: MCSessionState); | |
begin | |
TOSLog.d('didChangeState'); | |
FPlatformMultipeer.SessionDidChangeState(session, peer, didChangeState); | |
end; | |
{ TPlatformMultipeer } | |
constructor TPlatformMultipeer.Create(const AServiceType: string; const ADisplayName: string = ''); | |
var | |
LDisplayName: NSString; | |
begin | |
inherited Create; | |
//!!!!! Not using assistant // FUseAssistant := True; | |
{$IF not Defined(IOS)} | |
FViewController := TNSViewController.Alloc; | |
{$ENDIF} | |
FAdvertiserDelegate := TMCNearbyServiceAdvertiserDelegate.Create(Self); | |
FAssistantDelegate := TMCAdvertiserAssistantDelegate.Create(Self); | |
FBrowserDelegate := TMCNearbyServiceBrowserDelegate.Create(Self); | |
FSessionDelegate := TMCSessionDelegate.Create(Self); | |
FBrowserViewControllerDelegate := TMCBrowserViewControllerDelegate.Create(Self); | |
FServiceType := StrToNSStr(AServiceType); | |
if ADisplayName.IsEmpty then | |
LDisplayName := GetDeviceName | |
else | |
LDisplayName := StrToNSStr(ADisplayName); | |
FPeerID := TMCPeerID.Alloc; | |
FPeerID := TMCPeerID.Wrap(FPeerID.initWithDisplayName(LDisplayName)); | |
FSession := TMCSession.Alloc; | |
FSession := TMCSession.Wrap(FSession.initWithPeer(FPeerID)); | |
FSession.setDelegate(FSessionDelegate.GetObjectID); | |
FBrowser := TMCNearbyServiceBrowser.Alloc; | |
FBrowser := TMCNearbyServiceBrowser.Wrap(FBrowser.initWithPeer(FPeerID, FServiceType)); | |
FBrowser.setDelegate(FBrowserDelegate.GetObjectID); | |
FBrowserViewController := TMCBrowserViewController.Alloc; | |
FBrowserViewController := TMCBrowserViewController.Wrap(FBrowserViewController.initWithServiceType(FServiceType, FSession)); | |
FBrowserViewController.setDelegate(FBrowserViewControllerDelegate.GetObjectID); | |
end; | |
destructor TPlatformMultipeer.Destroy; | |
begin | |
FAdvertiserDelegate.Free; | |
FAssistantDelegate.Free; | |
FBrowserDelegate.Free; | |
FSessionDelegate.Free; | |
FBrowserViewControllerDelegate.Free; | |
inherited; | |
end; | |
procedure TPlatformMultipeer.DoConfirmPeerInvite(const ADisplayName: string; const AResponse: TConfirmPeerInviteResponse); | |
begin | |
if Assigned(FOnConfirmPeerInvite) then | |
FOnConfirmPeerInvite(Self, ADisplayName, AResponse) | |
else | |
AResponse(False); | |
end; | |
procedure TPlatformMultipeer.DismissBrowser; | |
begin | |
{$IF Defined(IOS)} | |
FBrowserViewController.dismissViewControllerAnimated(True, nil); | |
{$ELSE} | |
// Needs to be wrapped to match the same class as defined in DW.Macapi.AppKit | |
FViewController.dismissViewController(TNSViewController.Wrap(NSObjectToID(FBrowserViewController))); | |
{$ENDIF} | |
end; | |
procedure TPlatformMultipeer.Start; | |
begin | |
TOSLog.d('TPlatformMultipeer.Start'); | |
if not FIsStarted then | |
begin | |
FIsUsingAssistant := FUseAssistant; | |
if FIsUsingAssistant then | |
StartAssistant | |
else | |
StartAdvertiser; | |
FBrowser.startBrowsingForPeers; | |
FIsStarted := True; | |
end; | |
end; | |
procedure TPlatformMultipeer.StartAdvertiser; | |
begin | |
FAdvertiser := TMCNearbyServiceAdvertiser.Alloc; | |
FAdvertiser := TMCNearbyServiceAdvertiser.Wrap(FAdvertiser.initWithPeer(FPeerID, nil, FServiceType)); | |
FAdvertiser.setDelegate(FAdvertiserDelegate.GetObjectID); | |
FAdvertiser.startAdvertisingPeer; | |
end; | |
procedure TPlatformMultipeer.StartAssistant; | |
begin | |
FAssistant := TMCAdvertiserAssistant.Alloc; | |
FAssistant := TMCAdvertiserAssistant.Wrap(FAssistant.initWithServiceType(FServiceType, nil, FSession)); | |
FAssistant.setDelegate(FAssistantDelegate.GetObjectID); | |
FAssistant.start; | |
end; | |
procedure TPlatformMultipeer.Stop; | |
begin | |
if FIsStarted then | |
begin | |
if FIsUsingAssistant then | |
begin | |
FAssistant.stop; | |
FAssistant := nil; | |
end | |
else | |
begin | |
FAdvertiser.stopAdvertisingPeer; | |
FAdvertiser := nil; | |
end; | |
FBrowser.stopBrowsingForPeers; | |
FIsStarted := False; | |
end; | |
end; | |
procedure TPlatformMultipeer.SessionDidChangeState(const ASession: MCSession; const APeer: MCPeerID; const AState: MCSessionState); | |
const | |
cSessionStateCaption: array[0..2] of string = ('Not Connected', 'Connecting', 'Connected'); | |
begin | |
// Apparently this can occur outside of the UI thread | |
TOSLog.d('%s: %s', [NSStrToStr(APeer.displayName), cSessionStateCaption[AState]]); | |
end; | |
procedure TPlatformMultipeer.ShowBrowser; | |
begin | |
{$IF Defined(IOS)} | |
TiOSHelper.SharedApplication.keyWindow.rootViewController.presentViewController(FBrowserViewController, True, nil); | |
{$ELSE} | |
FViewController.setView(WindowHandleToPlatform(Application.MainForm.Handle).View); | |
// Needs to be wrapped to match the same class as defined in DW.Macapi.AppKit | |
FViewController.presentViewControllerAsSheet(TNSViewController.Wrap(NSObjectToID(FBrowserViewController))); | |
{$ENDIF} | |
end; | |
procedure TPlatformMultipeer.AssistantDidDismissInvitation(const AAssistant: MCAdvertiserAssistant); | |
begin | |
end; | |
procedure TPlatformMultipeer.AssistantWillPresentInvitation(const AAssistant: MCAdvertiserAssistant); | |
begin | |
end; | |
procedure TPlatformMultipeer.BrowserDidNotStartBrowsingForPeers(const ABrowser: MCNearbyServiceBrowser; const AError: NSError); | |
begin | |
end; | |
procedure TPlatformMultipeer.BrowserFoundPeer(const ABrowser: MCNearbyServiceBrowser; const APeer: MCPeerID; const ADiscoveryInfo: NSDictionary); | |
begin | |
// Auto-invite. Might not want to do this :-) | |
// ABrowser.invitePeer(APeer, FSession, nil, 10); | |
end; | |
procedure TPlatformMultipeer.BrowserLostPeer(const ABrowser: MCNearbyServiceBrowser; const APeer: MCPeerID); | |
begin | |
end; | |
end. |
Updated to use the Assistant by default. Set FUseAssistant to False if using Advertiser, but be aware of the crash (as noted in the code comments)
Resolved method of showing nearby browser on macOS - NOTE: Now requires DW.Macapi.AppKit from Kastri
Changes:
- Removed dependencies on DW.OSDevice and DW.OSLog.
- Fixed problem with nearby browser not showing devices
- Implemented acceptance of certificate, allowing connection to complete
- Logs connection status
To do:
- Manage connection/disconnection of peers
- Solve the crash re: invites
Changes:
- Solved crash re: invites
- Switched from using Assistant to using Advertiser
- Added event to allow for customization of invite confirmation
Example of an event handler for invite confirmation:
uses
FMX.DialogService.Async;
procedure TForm1.MultipeerConfirmPeerInviteHandler(Sender: TObject; const ADisplayName: string; const AResponse: TConfirmPeerInviteResponse);
begin
TDialogServiceAsync.MessageDialog(Format('Accept invitation from %s?', [ADisplayName]), TMsgDlgType.mtConfirmation, mbYesNo, TMsgDlgBtn.mbNo, 0,
procedure(const AResult: TModalResult)
begin
AResponse(AResult = mrYes);
end
);
end;
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Note that this code requires files from the Kastri library