Skip to content

Instantly share code, notes, and snippets.

@DelphiWorlds
Last active July 31, 2022 20:15
Show Gist options
  • Select an option

  • Save DelphiWorlds/912a3020502ec4f283ae36cd36bc6113 to your computer and use it in GitHub Desktop.

Select an option

Save DelphiWorlds/912a3020502ec4f283ae36cd36bc6113 to your computer and use it in GitHub Desktop.
Work in progress implementation of multipeer connectivity for iOS and macOS
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.
@DelphiWorlds
Copy link
Copy Markdown
Author

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)

@DelphiWorlds
Copy link
Copy Markdown
Author

Resolved method of showing nearby browser on macOS - NOTE: Now requires DW.Macapi.AppKit from Kastri

@DelphiWorlds
Copy link
Copy Markdown
Author

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

@DelphiWorlds
Copy link
Copy Markdown
Author

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