I’ve been working on the screen casting for iOS recently, which is quite a struggle, to be honest. If you can directly use music broadcast SDK, it would be the best, after all, someone else professional.
DLNA has a basic material, you can refer to DLNA screen casting related analysis and DLNA based on iOS, Android screen casting: subscription event notification, they write more detailed articles.
According to the truth, if the screen is cast, it will be played directly on the TV, and the operation on the TV side will not be received by the mobile phone. But now, the demand is to synchronize. So we need to know the state of the TV.
According to the two articles given, there are two methods: polling and subscription.
In the case of polling, we need to implement a scheduled task to constantly query the playing status and progress of the device. I failed in this method, and it was easy to cause a loop. I didn’t have a set of mechanisms to deal with it. If someone has already implemented it, you can share the solution.
I use a subscription approach, local service, listening port.
Of course, you need a GCDWebServer. The library we’re using, by the way, is MRDLNA. (I’m using CLUPnp content directly, the library encapsulation is just so-so).
Import GCDWebServer, POD immediately implemented.
Starting the local Server
_listener = [GCDWebServer new];
_listener.delegate = self;
WS(weakSelf);
[_listener addHandlerForMethod:@"NOTIFY" path:@"/dlna/callback"requestClass:[GCDWebServerDataRequest class] processBlock:^GCDWebServerResponse * _Nullable(__kindof GCDWebServerRequest * _Nonnull request) {if (request && [request hasBody]) {
[weakSelf parseWebServerData:((GCDWebServerDataRequest *)request).data];
}
return [[GCDWebServerDataResponse alloc] initWithHTML:@"<html><body><p>Hello World</p></body></html>"];
}];
[_listener startWithPort:8899 bonjourName:nil];
Copy the code
In this case, we’re listening on port 8899.
Send a subscription request to the device.
In this step, we send a subscription message to the device, including the address and port to receive the callback, and this address and port, naturally, is our device on the phone. After we start webServer, we already know the phone’s address and listening port.
Before we subscribe, we have to successfully cast the screen, and when we do, we subscribe. All the information about the device is in the CLUPnPDevice class.
// URLHeader-- ADDRESS of the TV device, eventSubURL-- url of the event NSString * URL = [NSString stringWithFormat:@"% @ % @", device.URLHeader, device.AVTransport.eventSubURL];
NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]];
req.HTTPMethod = @"SUBSCRIBE";
[req addValue:@"IOS / 9.2.1 UPnP/SCDLNA / 1.1 1.0" forHTTPHeaderField:@"User-Agent"]; // Via webServer, get http://*.*.*.*:*/ [req addValue:[NSString stringWithFormat:@"<%@dlna/callback>", [_listener serverURL].absoluteString] forHTTPHeaderField:@"CALLBACK"];
[req addValue:@"upnp:event" forHTTPHeaderField:@"NT"];
[req addValue:@"Second-3600" forHTTPHeaderField:@"TIMEOUT"];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:req completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if(error || ! data || ! Response) {// Failed to subscribe device.sid = @"";
} else {
NSHTTPURLResponse *res = (NSHTTPURLResponse *)response;
if(res.statusCode == 200) {// Subscribed successfully NSString *sid = res.allHeaderFields[@"SID"]? res.allHeaderFields[@"SID"] : @"";
self.lastSubscribedID = sid;
device.sid = sid;
} else {
device.sid = @""; }}}]; [task resume]; });Copy the code
The sid obtained here is in the format uUID :**-**-**-**-**.
Receive subscription information.
The main thing is to deal with the parseWebServerData method, where XML is parsed (typically using three parties, such as GDtataXML) to get the value.
Deal with it first. And & gt; , mainly for compatibility with scenarios.
NSString *originStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSString *tranferLeft = [originStr stringByReplacingOccurrencesOfString:@"<" withString:@"<"];
NSString *result = [tranferLeft stringByReplacingOccurrencesOfString:@">" withString:@">"];
Copy the code
After processing, we get the string of XML:
<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">
<e:property>
<LastChange>
<Event xmlns="urn:schemas-upnp-org:metadata-1-0/AVT/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:schemas-upnp-org:metadata-1-0/AVT/http://www.upnp.org/schemas/av/avt-event-v1-20060531.xsd">
<InstanceID val="0">
<TransportState val="PLAYING"/>
<CurrentTransportActions val="Play,Stop,Pause,Seek,X_DLNA_SeekTime,Next,Previous"/>
<CurrentTrackDuration val="00:23:20"/>
</InstanceID>
</Event>
</LastChange>
</e:property>
</e:propertyset>
Copy the code
From the string at 👆, we can see that we can only listen for this data, mainly the playback state of the device. The state TransportState is resolved.
GDataXMLDocument *document = [[GDataXMLDocument alloc] initWithXMLString:result options:0 error:&error];
if(! error) { GDataXMLElement *root = document.rootElement; GDataXMLElement *property = [root elementsForName:@"e:property"].firstObject;
if(! property) {return;
}
GDataXMLElement *lastChange = [property elementsForName:@"LastChange"].firstObject;
if(! lastChange) {return;
}
GDataXMLElement *event = [lastChange elementsForName:@"Event"].firstObject;
if(! event) {return;
}
GDataXMLElement *instance = [event elementsForName:@"InstanceID"].firstObject;
if(! instance) {return;
}
GDataXMLElement *states = [instance elementsForName:@"TransportState"].firstObject;
if(! states) {return;
}
GDataXMLNode *node = [states attributeForName:@"val"];
if(node) {// For real string comparisons, uppercaseString is best. [self playingStatus:[node stringValue]]; }}Copy the code
Sometimes the console will print a parsing failure. It should be an XML problem. I simply ignore it because the information we really need can be parsed successfully.
The problem is that the device state is available, but what about the playback progress of the device? This is a giant crater.
The current solution is that after obtaining the status of the device, the action is mainly sent to request the playing progress of the device.
[_render getPositionInfo];
Copy the code
CLUPnPAction *action = [[CLUPnPAction alloc] initWithAction:@"GetPositionInfo"];
[action setArgumentValue:@"0" forName:@"InstanceID"];
[self postRequestWith:action];
Copy the code
All of these are in the tripartite library, so I won’t list them here.
unsubscribe
Don’t forget to unsubscribe after you finish screening or disconnect. When we subscribe, we save a SID, which comes in handy.
NSString *url = [NSString stringWithFormat:@"% @ % @", device.URLHeader, device.AVTransport.eventSubURL];
NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]];
req.HTTPMethod = @"UNSUBSCRIBE";
[req addValue:_lastSubscribedID forHTTPHeaderField:@"SID"];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:req completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if(error || ! data || ! Response) {// Unsubscribe failed}else {
NSHTTPURLResponse *res = (NSHTTPURLResponse *)response;
if(res.statusCode == 200) {// Unsubscribe successfully self.lastSubscribedid = @"";
device.sid = @""; }}}]; [task resume]; });Copy the code
At present, I have done so far, the synchronization is not so precise, and it needs to continue to adjust. I don’t know how the MUSIC broadcast SDK is implemented, I really want to know.
If you have any questions about this article, or if you have 💡, please feel free to reply. Have their own implementation of polling, welcome to share the scheme.