TcpClient or HttpWebRequest to Apple TV ending aft

2019-02-17 13:15发布

问题:

I'm working on creating an Library in C# to use the Airplay protocol to send Photos and Video to my Apple TV (Specifically working with Generation 3 but hopefully that should not matter for this).

https://airlib.codeplex.com/

All of the commands for Airplay are HTTP on port 70 as per this spec: http://nto.github.com/AirPlay.html

I have been successful at getting both photos and video to play on the Apple TV, but no matter what I do the AppleTV will only play 30 seconds worth of video. It appears as though my C# client that issues the play command is disconnecting right at 30 seconds, which causes the AppleTV to end the play session.

Reasons why I think this:

  • Terminating the client app completely produces the same behavior as waiting 30 seconds (essentially forcing the connection to close).
  • Manually closing the HttpWebRequest or TcpClient connection produces the same behavior (Midway through a play session).
  • Regardless of how long I hold the breakpoint to prevent the GetResponse() call the video always times out 30 seconds after the WebRequest begins sending the message.
  • Using a different source (IIS, external webserver) for the video does not change the behavior.
  • Even after the video has cached on the AppleTV and does not re-stream the timeout still occurs.

I'm pretty sure that the client request needs to stay connected throughout the "play" of the video, and to the best of my knowledge I have coded it to do that. I really am at my wits end. I have tried everything that I can think of including doing the request both as a HttpWebRequest and as a raw TcpClient (which both work but both time out), setting the Recieve/Send timeouts to crazy numbers, and looping the read of the Tcp stream to ensure that there is "activity".

Its as though the AppleTV is expecting me to send a "hey, keep playing" message, but I have yet to see anything like that from any source on the web. I'm hoping that this is simply something stupid that I'm not doing based on my lack of Http/Tcp knowledge.

Here is my code:

    Uri url = "http://somevideo.com/video.mov";
    float startPosition = 0;        
    TcpClient tcpClient = new TcpClient("192.168.1.20",7000);
    tcpClient.ReceiveTimeout = 100000;
    tcpClient.SendTimeout = 100000;

    //get the client stream to read data from.
    NetworkStream clientStream = tcpClient.GetStream();

     string body = 
    "Content-Location: " + url + "\n" +
    "Start-Position: " + startPosition + "\n";

    string request = "POST /play HTTP/1.1\n" + 
    "User-Agent: MediaControl/1.0\n" +
    "Content-Type: text/parameters\n" +
    "Content-Length: " + Encoding.ASCII.GetBytes(body).Length + "\n" +           
    "X-Apple-Session-ID:" + _sessionGuid.ToString() + "\n\n";

    sendMessage(clientStream, request);
    sendMessage(clientStream, body);

    byte[] myReadBuffer = new byte[1024];
    StringBuilder myCompleteMessage = new StringBuilder();
    int numberOfBytesRead = 0;

    //incoming message might be bigger than the buffer
    do
    {
        try
        {
            numberOfBytesRead = clientStream.Read(myReadBuffer, 0, myReadBuffer.Length);
            myCompleteMessage.Append(Encoding.ASCII.GetString(myReadBuffer, 0, numberOfBytesRead));
            Thread.Sleep(10);//let the iOS device catch up sending data
        }
        catch (System.IO.IOException) { }
    } while (tcpClient.Connected); //check if it's connected before checking for data available, as maybe the program might get quit and the sockets closed halfway through a read

Note: using telnet I am able to connect to the AppleTV on port 7000 and paste in this command which plays the entire video:

POST /play HTTP/1.1
User-Agent: MediaControl/1.0
Content-Type: text/parameters
Content-Length: 89
X-Apple-Session-ID:fb6d816a-a5ad-4e8f-8830-9642b6e6eb35

Content-Location: http://192.168.1.11:82/2012/2012_03_11/IMG_1328.MOV
Start-Position: 0

I'm running the Cassini Webserver on port 82, but this also works with IIS. This offers further evidence that the .Net stack is doing something under the hood at 30 seconds that causes a disconnect.

回答1:

I figured it out finally. It wasn't the .Net code killing the connection, it was the Apple TV itself. With wireshark I was able to see the proper Ack and Fin messages which were from the AppleTV after 30 seconds of not recieving any new messages on that connection. To solve the problem I figured out by playing around with Telnet that the AppleTV doesn't seem to care what you send it as long as you send it SOMETHING on a periodic basis, which seems to keep the connection alive.

With HttpWebRequest the send/recieve portion is pretty canned. Its designed for a standard Http request and response, and if you need to do anything else you simply start a new HttpWebRequest rather than use the existing one. Trying to send a 2nd message on the same HttpWebRequest errors out.

So I had to use a TcpClient, and had to rework the end.

    /// <summary>
    /// Starts a video.
    /// </summary>
    /// <param name="url">The URL of the video to play.</param>
    /// <param name="startPosition">The start position of the video. This value must be between 0 and 1</param>
    public void StartVideo(Uri url, float startPosition = 0)
    {
        if (startPosition > 1)
        {
            throw new ArgumentException("Start Position must be between 0 and 1");
        }

        TcpClient tcpClient = new TcpClient("192.168.1.20", 7000);
        tcpClient.ReceiveTimeout = 100000;
        tcpClient.SendTimeout = 100000;

        //get the client stream to read data from.
        NetworkStream clientStream = tcpClient.GetStream();

        string body =
       "Content-Location: " + url + "\n" +
       "Start-Position: " + startPosition + "\n";

        string request = "POST /play HTTP/1.1\n" +
        "User-Agent: MediaControl/1.0\n" +
        "Content-Type: text/parameters\n" +
        "Content-Length: " + Encoding.ASCII.GetBytes(body).Length + "\n" +
        "X-Apple-Session-ID:" + _sessionGuid.ToString() + "\n\n";

        //Send the headers
        sendMessage(clientStream, request);
        //Send the body
        sendMessage(clientStream, body);

        //Get the response
        byte[] myReadBuffer = new byte[1024];
        StringBuilder myCompleteMessage = new StringBuilder();
        int numberOfBytesRead = 0;
        numberOfBytesRead = clientStream.Read(myReadBuffer, 0, myReadBuffer.Length);
        myCompleteMessage.Append(Encoding.ASCII.GetString(myReadBuffer, 0, numberOfBytesRead));

        //Now start doing a "keepalive"
        while (true)
        {
            //Simply send the characters "ok" every two seconds
            sendMessage(clientStream, "ok");
            Thread.Sleep(2000);
        }                      
    }

    /// <summary>
    /// Sends a message across the NetworkStream
    /// </summary>
    /// <param name="clientStream">The stream to send the message down</param>
    /// <param name="message">The message to send</param>
    public void sendMessage(NetworkStream clientStream, string message)
    {
        byte[] buffer = new ASCIIEncoding().GetBytes(message);
        try
        {
            clientStream.Write(buffer, 0, buffer.Length);
            clientStream.Flush();
        }
        catch (System.IO.IOException e)
        {
            Debug.WriteLine("IOException: " + e.Message);
        }
    }

Obviously this is not the final answer, but this was the bare minimum to get it to work. If anybody figures out what the actual Apple hardware is sending in place of the "ok" please add a note.