Rollo62 536 Posted May 14, 2020 Hi there, I was trying to extend the iOSapi_AVFoundation.pas module, by adding the following function to the AVAudioSession function setCategoryWithOptionsError( category: NSString; withOptions : AVAudioSessionCategoryOptions; error: NSError): Boolean; cdecl; Unfortunately its crashing with an object is nil error, what do I miss here ? I make a local copy of the unit, and the following additions: uses ... const AVAudioSessionCategoryOptionDuckOthers = 2; //S4: add 14.05.20 ... type AVAudioSessionCategoryOptions = NSUInteger; //S4: add 14.05.20 AVAudioSessionClass = interface(NSObjectClass) ['{B24932F9-3C98-44E4-A4F6-0CB58AF7DE8A}'] //S4: 14.05.20 new GUID {class} function sharedInstance: Pointer; cdecl; end; AVAudioSession = interface(NSObject) ['{0B02D5EC-ED09-421A-84BE-6CEE08420E14}'] //S4: 14.05.20 new GUID ... //S4: Addition 14.05.20, the new function I need //[MethodName('setCategory:withOptions:error:')] function setCategoryWithOptionsError( category: NSString; withOptions : AVAudioSessionCategoryOptions; error: NSError): Boolean; cdecl; ... end. This is derived from the function in the Apple reference https://developer.apple.com/documentation/avfoundation/avaudiosession/1616442-setcategory Could be looking in Objective-C like this AVAudioSessionCategoryOptions AVAudioSessionCategoryOptionsNone = 0; [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionsNone error:nil]; I want to call it like this LAVAudioSession := TAVAudioSession.Wrap( TAVAudioSession.OCClass.sharedInstance ); //<-- This is original method, it still can be called without exception LRes := LAVAudioSession.setCategory( AVAudioSessionCategoryPlayback, LError); //<-- This is the new method, crash with exception LRes := LAVAudioSession.setCategoryWithOptionsError( AVAudioSessionCategoryPlayback, AVAudioSessionCategoryOptionDuckOthers, LError); Do I have a stupid typo somewhere ? Can the two modules not coexist, because the old one is linked somewhere else ? Shall I use different GUID for the interfaces, or keep the same, to allow static links to find them ? Is the new method not yet in the Rx10.3.3 libraries, but I updated to latest SDK 13.4.1, they should be in there and FMX able to resolve it ? Does the module need to be initialized somehow ? Is there something missing what is needed for correct linkage ? Maybe its too late already, or I'm to blind to see. Share this post Link to post
Dave Nottage 557 Posted May 14, 2020 4 hours ago, Rollo62 said: function setCategoryWithOptionsError( category: NSString; withOptions : AVAudioSessionCategoryOptions; error: NSError😞 Boolean; cdecl; It should be: function setCategoryWithOptionsError(category: NSString; withOptions: AVAudioSessionCategoryOptions; error: PPointer): Boolean; cdecl; But you don't show how you're calling it, either. A possible example: var LErrorPtr: Pointer; LError: NSError; ... setCategoryWithOptionsError(CocoaNSStringConst(libAVFoundation, 'AVAudioSessionCategoryPlayAndRecord'), 0, @LErrorPtr); if LErrorPtr <> nil then begin LError := TNSError.Wrap(LErrorPtr); // Deal with LError here end; 4 hours ago, Rollo62 said: Shall I use different GUID for the interfaces, You should. 4 hours ago, Rollo62 said: Is the new method not yet in the Rx10.3.3 libraries, No. I suggest filing a QP report. 1 Share this post Link to post
Rollo62 536 Posted May 15, 2020 (edited) @Dave Nottage Thanks for the correction, I will check this out. What I really dont understand why this should be different. There is a prototype in the iOSapi_Foundation.pas file already, the setCategory function setCategory(theCategory: NSString; error: NSError): Boolean; cdecl; It uses the error: NSError type I simply tried to copy that, and extent to function setCategoryWithOptionsError( category: NSString; withOptions : AVAudioSessionCategoryOptions; error: NSError): Boolean; cdecl; Why should it be error: PPointer, right now ? function setCategoryWithOptionsError(category: NSString; withOptions: AVAudioSessionCategoryOptions; error: PPointer): Boolean; cdecl; Also the FmxExpress header conversions from @Eli M. shows it with special NSError type. Edit: NSError is defined in iOSap_Foundation as interface PPointer ist jus a pointer to pointer in System NSError = interface(NSObject) ['{8E8F832A-5F75-4F65-A18B-5A8E2F49A867}'] function code: NSInteger; cdecl; function domain: NSString; cdecl; function helpAnchor: NSString; cdecl; function initWithDomain(domain: NSString; code: NSInteger; userInfo: NSDictionary): Pointer; cdecl; function localizedDescription: NSString; cdecl; function localizedFailureReason: NSString; cdecl; function localizedRecoveryOptions: NSArray; cdecl; function localizedRecoverySuggestion: NSString; cdecl; function recoveryAttempter: Pointer; cdecl; function userInfo: NSDictionary; cdecl; end; ... PPointer = ^Pointer; {$NODEFINE PPointer} { defined in sysmac.h } How can the type of "error" change in one unit, is this another Apple-Magic ? And the original libraries from Apple seems to use similar types as well, here are all the variants. Why to use NSError in one, and PPointer in the other, I don't get that ? func setCategory(_ category: AVAudioSession.Category) throws func setCategory(_ category: AVAudioSession.Category, options: AVAudioSession.CategoryOptions = []) throws func setCategory(_ category: AVAudioSession.Category, mode: AVAudioSession.Mode, options: AVAudioSession.CategoryOptions = []) throws func setCategory(_ category: AVAudioSession.Category, mode: AVAudioSession.Mode, options: AVAudioSession.CategoryOptions = []) throws I call this as shown above, those are predefined function, to get the necessary string key constant. which is defined in iOSapi_Foundation function AVAudioSessionCategoryPlayback: NSString; in caller unit var LError : NSError; LRes := LAVAudioSession.setCategory( AVAudioSessionCategoryPlayback, LError); Quote 13 hours ago, Rollo62 said: Shall I use different GUID for the interfaces, You should. I meant when I import make a local copy of the unit, which has hundrets or interfaces keeping untouched. Will it be enough to use different GUID only for those interfaces that have changed, not the ones that keep original ? Still then would be 2 references 8old unit, new unit) of units which may have same interface GUID. Do I have to change all the GUIDS in that local copy of the unit (that would be a little messy) ? Quote 13 hours ago, Rollo62 said: Is the new method not yet in the Rx10.3.3 libraries, No. I suggest filing a QP report. You mean feature request for the iOSapi_Foundation, right ? Its clear that the interface is missing in iOSapi_Foundation, but I was not sure if there is anything else might missing in the static libraries. Since I assume those are copied and get actually from the current system, this should always up to date. My understanding of the iOS-Delphi bridge is that SDK from current system is bounded via the interfaces to Delphi classes, or is there anything hidden inbetween that I haven't seen yet ? Edited May 15, 2020 by Rollo62 Share this post Link to post
Dave Nottage 557 Posted May 15, 2020 1 hour ago, Rollo62 said: There is a prototype in the iOSapi_Foundation.pas file already, the setCategory I think you mean in iOSapi.AVFoundation.pas. You're working under the assumption that the declaration is correct - it is not. 1 hour ago, Rollo62 said: Why should it be error: PPointer, right now ? Because the corresponding declaration in Objective-C is: - (BOOL)setCategory:(AVAudioSessionCategory)category error:(NSError **)outError Note the double asterisk. Because the Objective-C bridge is presently unable to handle wrapping this, a PPointer can be used in the way I described in my reply. As an example, see the code for the DownloadableContentPath method in FMX.InAppPurchase.iOS, which uses the setResourceValue method of NSURL, which is declared correctly. 1 hour ago, Rollo62 said: You mean feature request for the iOSapi_Foundation, right ? It's a QP report either way, however I'm referring to the missing declaration for the setCategoryWithOptionsError method of AVAudioSession (and any others methods that might be needed) in iOSapi.AVFoundation. 1 hour ago, Rollo62 said: Since I assume those are copied and get actually from the current system, No, that's not automatic. 1 hour ago, Rollo62 said: this should always up to date It would be nice if it were, however translating SDK framework headers is not trivial, and as you've observed declaration errors can happen, and/or the Objective-C bridge needs to be modified to handle situations where needed. Up until 10.3.2 (or somewhere close to that), the bridge could not even handle Objective-C blocks on macOS (it could on iOS). Share this post Link to post
Rollo62 536 Posted May 15, 2020 (edited) Quote You're working under the assumption that the declaration is correct - it is not. A yes, great. So I better check the c headers from now on, instead of Apple Docs. Every time I see a NSError now I will have to replace it bei PPointer, what a mess Then there were many wrong entries in the unit, I only wonder why this seems to work with setCategory, at least I get no exceptions. Unfortunately it still doesn't run without exception, and I think I've tried all optiions, see alternatives (A) to (C) and (1) to (5), I've tried most combinations with same result. When I don't use local copy with different name iOSapi_AVFoundation_Ex I do get compiler errors in FMX.Media.iOS, seems that one uses the original unit, and cannot work with my local copy. Thats why I used a different unit name, that also is working for other iOS units before. unit iOSapi_AVFoundation_Ex; //<-- local copy, with extensions and changed GUID var LAVAudioSession : AVAudioSession; LError : NSError; LRes : Boolean; (A) as PPointer, called by @LPError (B) as PPointer, called by LPError (although this makes not much sense) //LPError : PPointer; (C) as Pointer, called by @LPError LPError : Pointer; begin LAVAudioSession := TAVAudioSession.Wrap( TAVAudioSession.OCClass.sharedInstance ); // (1) this works as far as I can say, at least never see exceptions I can say LRes := LAVAudioSession.setCategory( AVAudioSessionCategoryPlayback, LError); // (2) first approach, throws exception LRes := LAVAudioSession.setCategoryWithOptionsError( AVAudioSessionCategoryPlayback, AVAudioSessionCategoryOptionDuckOthers, LError); // (3) Dave's proposal // Because the corresponding declaration in Objective-C is: // - (BOOL)setCategory:(AVAudioSessionCategory)category error:(NSError **)outError //PPointer = ^Pointer; {$NODEFINE PPointer} { defined in sysmac.h } //NSError = interface; // (A), (B) set to nil before LPError := nil; //<-- this always shows same crash, no matter what (A),(B),(C), (2)-(5) LRes := LAVAudioSession.setCategoryWithOptionsError( // (4a) this alternative both version should be identical -> get from string //CocoaNSStringConst(libAVFoundation, 'AVAudioSessionCategoryPlayback'), // (4b) this alternative get from dedicated function, doing same as above //AVAudioSessionCategoryPlayback, // (4c) this alternative from Dave's original proposal, using different mode CocoaNSStringConst(libAVFoundation, 'AVAudioSessionCategoryPlayAndRecord'), // (5a) this alternative is a simple integer constant 0x2, defined as const = 2 AVAudioSessionCategoryOptionDuckOthers, // 2 // (5b) this alternative also crashes same //2, // (5c) this alternative from Dave's original proposal, using different option 0, @LPError); if LPError <> nil then begin LError := TNSError.Wrap(LPError); end; if not LRes or (LError <> nil) then begin // Something wrong end; Edited May 15, 2020 by Rollo62 Share this post Link to post
Rollo62 536 Posted May 15, 2020 (edited) Quote declaration errors can happen, and/or the Objective-C bridge needs to be modified to handle situations where needed. Up until 10.3.2 (or somewhere close to that), the bridge could not even handle Objective-C blocks on macOS (it could on iOS). @Dave Nottage Or did I misunderstand your post, and you say this is currently under 10.3.3 NOT usable, even with the proposals you gave, because Objective-C bridge cannot handle it yet ? That would be the worst case, I need that urgently, my customers already ask why can't we do what other apps can do. I monitor the changes in all Delphi iOSApi and AndroidApi units since the last 5 or 6 versions, so far never changed anything since years, only copyright year is eagerly updated. My hope is that all the available system interfaces might get linked to FMX units soon. Is there maybe a helpful unit from the FPC community, at least this didn't jump in my eye yet ? Any solution welcome, to configure background and foreground audio. Edited May 15, 2020 by Rollo62 Share this post Link to post
Rollo62 536 Posted May 15, 2020 Here is something for FPC, but quite old. https://github.com/ObjectUp/CrossRoads/blob/master/CrossRoads_DelphiVER280_Port/LSNLibrary/unLSNSound.pas I'm not sure if and how binding FPC solution into Delphi makes sense, probable the iOS bridges were too different, but since the first version of FMX based on this I still have hope that there is a second way. Share this post Link to post
Dave Nottage 557 Posted May 15, 2020 13 hours ago, Rollo62 said: Every time I see a NSError now I will have to replace it bei PPointer, what a mess Every time you see a NSError** you'll need to replace it with PPointer. Also, if you see: (NSError * _Nullable * _Nullable) (Which amounts to the same thing) 12 hours ago, Rollo62 said: Or did I misunderstand your post, and you say this is currently under 10.3.3 NOT usable, even with the proposals you gave, I was just pointing out the incorrect declaration; I have not looked at any other aspect as yet. I'll take a look at your example today and see what the problem might be. Share this post Link to post
Dave Nottage 557 Posted May 16, 2020 2 hours ago, Dave Nottage said: I'll take a look at your example today and see what the problem might be. I should have realised from the beginning - it's a method resolution problem. The Objective-C method name is actually setCategory, not setCategoryWithOptionsError, so it needs a MethodName attribute. This executes without error for me: uses Macapi.ObjectiveC, Macapi.Helpers, iOSapi.Foundation, iOSapi.AVFoundation, iOSapi.CocoaTypes; const AVAudioSessionCategoryOptionNone = 0; AVAudioSessionCategoryOptionMixWithOthers = 1; AVAudioSessionCategoryOptionDuckOthers = 2; AVAudioSessionCategoryOptionAllowBluetooth = 4; AVAudioSessionCategoryOptionDefaultToSpeaker = 8; AVAudioSessionCategoryOptionInterruptSpokenAudioAndMixWithOthers = 17; AVAudioSessionCategoryOptionAllowBluetoothA2DP = 32; AVAudioSessionCategoryOptionAllowAirPlay = 64; type AVAudioSessionCategory = NSString; AVAudioSessionCategoryOptions = NSInteger; AVAudioSessionClass = interface(NSObjectClass) ['{F8B0F7A3-1805-4739-B827-3AE7FFCD20F0}'] {class} function sharedInstance: Pointer; cdecl; end; AVAudioSession = interface(NSObject) ['{C6EAD2A6-DE66-4A80-B019-8564FF6927AF}'] function category: NSString; cdecl; function currentHardwareInputNumberOfChannels: NSInteger; cdecl; function currentHardwareOutputNumberOfChannels: NSInteger; cdecl; function currentHardwareSampleRate: double; cdecl; function delegate: Pointer; cdecl; function inputIsAvailable: Boolean; cdecl; function mode: NSString; cdecl; function preferredHardwareSampleRate: double; cdecl; function preferredIOBufferDuration: NSTimeInterval; cdecl; function setActive(beActive: Boolean; error: PPointer): Boolean; cdecl; overload; function setActive(beActive: Boolean; withFlags: NSInteger; error: PPointer): Boolean; cdecl; overload; function setCategory(theCategory: NSString; error: PPointer): Boolean; cdecl; [MethodName('setCategory:withOptions:error:')] function setCategoryWithOptionsError(category: AVAudioSessionCategory; withOptions: AVAudioSessionCategoryOptions; error: PPointer): Boolean; cdecl; procedure setDelegate(delegate: Pointer); cdecl; function setMode(theMode: NSString; error: PPointer): Boolean; cdecl; function setPreferredHardwareSampleRate(sampleRate: double; error: PPointer): Boolean; cdecl; function setPreferredIOBufferDuration(duration: NSTimeInterval; error: PPointer): Boolean; cdecl; end; TAVAudioSession = class(TOCGenericImport<AVAudioSessionClass, AVAudioSession>) end; procedure TForm1.Button1Click(Sender: TObject); var LSession: AVAudioSession; LErrorPtr: Pointer; begin LErrorPtr := nil; LSession := TAVAudioSession.Wrap(TAVAudioSession.OCClass.sharedInstance); if LSession.setCategoryWithOptionsError(AVAudioSessionCategoryPlayback, AVAudioSessionCategoryOptionNone, @LErrorPtr) then Label1.Text := 'Set category OK' else if LErrorPtr <> nil then Label1.Text := NSStrToStr(TNSError.Wrap(LErrorPtr).localizedDescription); end; Share this post Link to post
Rollo62 536 Posted May 16, 2020 11 hours ago, Dave Nottage said: it needs a MethodName attribute. This executes without error for me: A right. I even hat that in my code in the first place, but stupid me commented that out. So I will try again. Share this post Link to post