Skip to content

Instantly share code, notes, and snippets.

@DelphiWorlds
Last active July 31, 2022 20:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save DelphiWorlds/912a3020502ec4f283ae36cd36bc6113 to your computer and use it in GitHub Desktop.
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
Author

Note that this code requires files from the Kastri library

@DelphiWorlds
Copy link
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
Author

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

@DelphiWorlds
Copy link
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
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