Skip to content

Commit

Permalink
youtube playback fix
Browse files Browse the repository at this point in the history
  • Loading branch information
serezhka committed Feb 7, 2023
1 parent 6899d55 commit dc4092f
Show file tree
Hide file tree
Showing 39 changed files with 562 additions and 46 deletions.
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ Create `application.properties` file in working dir
```properties
# airplay
airplay.serverName=srzhka
airplay.airplayPort=15614
airplay.airtunesPort=5001
airplay.width=1280
airplay.height=720
Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ ext {
springBootVersion = '3.0.0'
springDepMgmtVersion = '1.1.0'
junitVersion = '5.9.1'
nettyVersion = '4.1.85.Final'
nettyVersion = '4.1.86.Final'
slf4jVersion = '2.0.5'
jnaVersion = '5.12.1'
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ public AirPlayBonjour(String serverName) {
this.serverName = serverName;
}

public void start(int airPlayPort, int airTunesPort) throws Exception {
public void start(int airTunesPort) throws Exception {
airPlayService = ServiceInfo.create(serverName + AIRPLAY_SERVICE_TYPE,
serverName, airPlayPort, 0, 0, airPlayMDNSProps());
serverName, airTunesPort, 0, 0, airPlayMDNSProps());
JmmDNS.Factory.getInstance().registerService(airPlayService);
log.info("{} service is registered on port {}", serverName + AIRPLAY_SERVICE_TYPE, airPlayPort);
log.info("{} service is registered on port {}", serverName + AIRPLAY_SERVICE_TYPE, airTunesPort);

String airTunesServerName = "010203040506@" + serverName;
airTunesService = ServiceInfo.create(airTunesServerName + AIRTUNES_SERVICE_TYPE,
Expand All @@ -48,27 +48,32 @@ public void stop() {
private Map<String, String> airPlayMDNSProps() {
HashMap<String, String> airPlayMDNSProps = new HashMap<>();
airPlayMDNSProps.put("deviceid", "01:02:03:04:05:06");
airPlayMDNSProps.put("features", "0x5A7FFFF7,0x1E");
airPlayMDNSProps.put("features", "0x5A7FFFE4,0x1E"); // 0x5A7FFFF7
airPlayMDNSProps.put("srcvers", "220.68");
airPlayMDNSProps.put("flags", "0x4");
airPlayMDNSProps.put("vv", "2");
airPlayMDNSProps.put("model", "AppleTV2,1");
airPlayMDNSProps.put("model", "AppleTV3,2");
airPlayMDNSProps.put("rhd", "5.6.0.0");
airPlayMDNSProps.put("pw", "false");
airPlayMDNSProps.put("pk", "b07727d6f6cd6e08b58ede525ec3cdeaa252ad9f683feb212ef8a205246554e7");
airPlayMDNSProps.put("pi", "2e388006-13ba-4041-9a67-25dd4a43d536");
airPlayMDNSProps.put("rmodel", "PC1.0");
airPlayMDNSProps.put("rrv", "1.01");
airPlayMDNSProps.put("rsv", "1.00");
airPlayMDNSProps.put("pcversion", "1715");
return airPlayMDNSProps;
}

private Map<String, String> airTunesMDNSProps() {
HashMap<String, String> airTunesMDNSProps = new HashMap<>();
airTunesMDNSProps.put("ch", "2");
airTunesMDNSProps.put("cn", "0,1,2,3");
airTunesMDNSProps.put("cn", "0,1,3");
airTunesMDNSProps.put("da", "true");
airTunesMDNSProps.put("et", "0,3,5");
airTunesMDNSProps.put("ek", "1");
airTunesMDNSProps.put("vv", "2");
airTunesMDNSProps.put("ft", "0x5A7FFFF7,0x1E");
airTunesMDNSProps.put("am", "AppleTV2,1");
airTunesMDNSProps.put("am", "AppleTV3,2");
airTunesMDNSProps.put("md", "0,1,2");
airTunesMDNSProps.put("rhd", "5.6.0.0");
airTunesMDNSProps.put("pw", "false");
Expand All @@ -79,7 +84,7 @@ private Map<String, String> airTunesMDNSProps() {
airTunesMDNSProps.put("txtvers", "1");
airTunesMDNSProps.put("sf", "0x4");
airTunesMDNSProps.put("vs", "220.68");
airTunesMDNSProps.put("vn", "65537");
airTunesMDNSProps.put("vn", "3"); // 65537
airTunesMDNSProps.put("pk", "b07727d6f6cd6e08b58ede525ec3cdeaa252ad9f683feb212ef8a205246554e7");
return airTunesMDNSProps;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public void info(int width, int height, int fps, OutputStream out) throws Except
response.put("displays", displays);
response.put("features", 130367356919L);
response.put("keepAliveSendStatsAsBody", 1);
response.put("model", "AppleTV2,1");
response.put("model", "AppleTV3,2");
response.put("name", "Apple TV");
response.put("pi", "b08f5a79-db29-4384-b456-a4784d9e6055");
response.put("sourceVersion", "220.68");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,6 @@
"type": "java.lang.String",
"description": "Name of airplay server."
},
{
"name": "airplay.airplayPort",
"type": "java.lang.String",
"description": "Airplay port."
},
{
"name": "airplay.airtunesPort",
"type": "java.lang.String",
Expand Down
5 changes: 2 additions & 3 deletions player/app/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,16 @@ logging.level.org.springframework.web=INFO
logging.level.javax.jmdns=WARN
logging.level.com.github.serezhka=DEBUG
logging.level.io.netty=INFO
logging.level.io.netty.handler.logging.LoggingHandler=WARN
logging.level.io.netty.handler.logging.LoggingHandler=DEBUG
logging.level.org.freedesktop.gstreamer=INFO
spring.output.ansi.enabled=ALWAYS
# airplay
airplay.serverName=srzhka
airplay.airplayPort=15614
airplay.airtunesPort=5001
airplay.width=1920
airplay.height=1080
airplay.fps=60
# player
# player (gstreamer, ffmpeg, vlc, h264-dump)
player.implementation=gstreamer
player.tray.enabled=true
player.gstreamer.swing=false
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public class FFmpegPlayer implements AirPlayConsumer {
@Override
public void onVideoFormat(VideoStreamInfo videoStreamInfo) {
try {
ProcessBuilder pb = new ProcessBuilder("ffplay", "-f", "h264", "-codec:v", "h264", "-probesize", "32",
ProcessBuilder pb = new ProcessBuilder("ffplay", "-fs", "-f", "h264", "-codec:v", "h264", "-probesize", "32",
"-analyzeduration", "0", "-vf", "setpts=0", "-flags", "low_delay", "-");
pb.redirectOutput(ProcessBuilder.Redirect.INHERIT);
pb.redirectError(ProcessBuilder.Redirect.INHERIT);
Expand All @@ -31,7 +31,7 @@ public void onVideo(byte[] bytes) {
h264Process.getOutputStream().write(bytes);
h264Process.getOutputStream().flush();
} catch (IOException e) {
e.printStackTrace();
log.error(e.getMessage());
}
}

Expand Down
2 changes: 2 additions & 0 deletions server/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@ dependencies {

implementation "io.netty:netty-all:$nettyVersion"
implementation "org.slf4j:slf4j-api:$slf4jVersion"

testImplementation 'com.googlecode.plist:dd-plist:1.26'
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
@Data
public class AirPlayConfig {
private String serverName;
private int airplayPort;
private int airtunesPort;
private int width;
private int height;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public AirPlayServer(AirPlayConfig airPlayConfig, AirPlayConsumer airPlayConsume
}

public void start() throws Exception {
airPlayBonjour.start(airPlayConfig.getAirplayPort(), airPlayConfig.getAirtunesPort());
airPlayBonjour.start(airPlayConfig.getAirtunesPort());
new Thread(controlServer).start();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,22 @@
import io.netty.channel.socket.DatagramChannel;
import io.netty.channel.socket.nio.NioDatagramChannel;
import io.netty.handler.codec.DatagramPacketDecoder;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import java.net.InetSocketAddress;

@Slf4j
@RequiredArgsConstructor
public class AudioReceiver implements Runnable {

private final AudioHandler audioHandler;
private final Object monitor;

@Getter
private int port;

public AudioReceiver(AudioHandler audioHandler, Object monitor) {
this.audioHandler = audioHandler;
this.monitor = monitor;
}

@Override
public void run() {
var bootstrap = new Bootstrap();
Expand Down Expand Up @@ -64,10 +63,6 @@ public void initChannel(final DatagramChannel ch) {
}
}

public int getPort() {
return port;
}

private EventLoopGroup eventLoopGroup() {
return Epoll.isAvailable() ? new EpollEventLoopGroup() : new NioEventLoopGroup();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public ControlServer(AirPlayConfig airPlayConfig, AirPlayConsumer airPlayConsume
SessionManager sessionManager = new SessionManager();
pairingHandler = new PairingHandler(airPlayConfig, sessionManager);
fairPlayHandler = new FairPlayHandler(sessionManager);
rtspHandler = new RTSPHandler(airPlayConfig.getAirplayPort(), airTunesPort, sessionManager, airPlayConsumer);
rtspHandler = new RTSPHandler(airTunesPort, sessionManager, airPlayConsumer);
heartBeatHandler = new HeartBeatHandler(sessionManager);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,21 @@
import io.netty.channel.socket.ServerSocketChannel;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import java.net.InetSocketAddress;

@Slf4j
@RequiredArgsConstructor
public class VideoReceiver implements Runnable {

private final int port;
private final VideoHandler videoHandler;
private final Object monitor;

public VideoReceiver(int port, VideoHandler videoHandler) {
this.port = port;
this.videoHandler = videoHandler;
}
@Getter
private int port;

@Override
public void run() {
Expand All @@ -37,7 +38,7 @@ public void run() {
serverBootstrap
.group(bossGroup, workerGroup)
.channel(serverSocketChannelClass())
.localAddress(new InetSocketAddress(port))
.localAddress(new InetSocketAddress(0)) // bind random port
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(final SocketChannel ch) {
Expand All @@ -49,7 +50,14 @@ public void initChannel(final SocketChannel ch) {
.childOption(ChannelOption.SO_REUSEADDR, true)
.childOption(ChannelOption.SO_KEEPALIVE, true);
var channelFuture = serverBootstrap.bind().sync();
log.info("AirPlay video receiver listening on port: {}", port);

log.info("AirPlay video receiver listening on port: {}",
port = ((InetSocketAddress) channelFuture.channel().localAddress()).getPort());

synchronized (monitor) {
monitor.notify();
}

channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
log.info("AirPlay video receiver interrupted");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,11 @@
public class RTSPHandler extends ControlHandler {

private final AirPlayConsumer airPlayConsumer;
private final int airPlayPort;
private final int airTunesPort;

public RTSPHandler(int airPlayPort, int airTunesPort, SessionManager sessionManager,
AirPlayConsumer airPlayConsumer) {
public RTSPHandler(int airTunesPort, SessionManager sessionManager, AirPlayConsumer airPlayConsumer) {
super(sessionManager);
this.airPlayConsumer = airPlayConsumer;
this.airPlayPort = airPlayPort;
this.airTunesPort = airTunesPort;
}

Expand Down Expand Up @@ -85,12 +82,15 @@ protected boolean handleRequest(ChannelHandlerContext ctx, Session session, Full
airPlayConsumer.onVideoFormat(videoStreamInfo);

var videoHandler = new VideoHandler(session.getAirPlay(), airPlayConsumer);
var videoReceiver = new VideoReceiver(airPlayPort, videoHandler);
var videoReceiver = new VideoReceiver(videoHandler, this);
var videoReceiverThread = new Thread(videoReceiver);
session.setVideoReceiverThread(videoReceiverThread);
videoReceiverThread.start();
synchronized (this) {
wait();
}

session.getAirPlay().rtspSetupVideo(new ByteBufOutputStream(response.content()), airPlayPort, airTunesPort, 7011);
session.getAirPlay().rtspSetupVideo(new ByteBufOutputStream(response.content()), videoReceiver.getPort(), airTunesPort, 7011);
break;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
import com.github.serezhka.airplay.server.internal.packet.VideoPacket;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@RequiredArgsConstructor
@ChannelHandler.Sharable
public class VideoHandler extends ChannelInboundHandlerAdapter {

private final AirPlay airPlay;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.github.serezhka.airplay.server;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.handler.codec.http.HttpMessage;
import io.netty.handler.codec.rtsp.RtspDecoder;

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;

public class RTSPUtil extends RtspDecoder /*HttpResponseDecoder*/ {

public static void main(String[] args) throws Exception {
RTSPUtil util = new RTSPUtil();
Path resource = Paths.get(RTSPUtil.class.getResource("/rtsp/set_parameter_x_dmap_tag.bin").toURI());
byte[] bytes = Files.readAllBytes(resource);
ByteBuf byteBuf = Unpooled.wrappedBuffer(bytes);

List<Object> result = new ArrayList<>();
util.decode(null, byteBuf, result);
util.decode(null, byteBuf, result);

HttpMessage message = (HttpMessage) result.get(0);
System.out.println(message);

// HttpContent content = (HttpContent) result.get(1);
// NSDictionary parsedContent = (NSDictionary) BinaryPropertyListParser.parse(new ByteBufInputStream(content.content()));
// NSDictionary parsedContent = (NSDictionary) PropertyListParser.parse(new ByteBufInputStream(content.content()));
// System.out.println(parsedContent.toXMLPropertyList());

// x-dmap-tagged
// ByteBuf buf = content.content();
// System.out.println("Tag: " + buf.readCharSequence(4, StandardCharsets.UTF_8));
// int size = buf.readInt();
// System.out.println("Size: " + size);
// System.out.println("Bytes left: " + buf.readableBytes());
}
}
Binary file added server/src/test/resources/rtsp/get_info_request.bin
Binary file not shown.
19 changes: 19 additions & 0 deletions server/src/test/resources/rtsp/get_info_request.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
GET /info RTSP/1.0
X-Apple-ProtocolVersion: 1
Content-Length: 70
Content-Type: application/x-apple-binary-plist
CSeq: 0
DACP-ID: 2C50405DC456F722
Active-Remote: 4147582535
User-Agent: AirPlay/670.6.2

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>qualifier</key>
<array>
<string>txtAirPlay</string>
</array>
</dict>
</plist>
Binary file added server/src/test/resources/rtsp/get_info_response.bin
Binary file not shown.
Loading

0 comments on commit dc4092f

Please sign in to comment.