Подтвердить что ты не робот

Загрузите видео в реальном времени на веб-камеру Android на сервер RTP/RTSP

Я уже провел правильные исследования, но мне все еще не хватает информации о том, чего я хотел бы достичь.

Поэтому я хотел бы запрограммировать приложение, в котором пользователь может записывать видео и мгновенно (жить) загружать видео на RTP/RTSP-сервер. Серверная сторона не будет проблемой. То, что я не понимаю, это то, как добиться этого на стороне телефона.

Мои исследования до сих пор состоят в том, что мне приходится записывать видео на запись в локальный сокет, а не в файл, потому что 3gp файлы, если они записаны в файл, не могут быть доступны, до завершения (когда видео остановлено и информация заголовка была записана в видео о длине и других).

Когда сокет получает непрерывные данные, мне нужно будет обернуть его в пакет RTP и отправить его на удаленный сервер. Возможно, мне также придется сначала выполнить базовую кодировку (что еще не так важно).

Есть ли у кого-нибудь идеи, если эта теория правильна до сих пор. Я также хотел бы узнать, может ли кто-нибудь указать мне несколько фрагментов кода подобных подходов, особенно для отправки видео "на лету" на сервер. Я еще не уверен, как это сделать.

Большое спасибо и наилучшие пожелания

4b9b3361

Ответ 1

Ваш общий подход звучит правильно, но есть несколько вещей, которые вам нужно учитывать.

Поэтому я хотел бы запрограммировать приложение, в котором пользователь может записывать видео и мгновенно (жить) загружать видео на RTP/RTSP-сервер.

  • Предполагаю, что вы хотите загрузить на сервер RTSP, чтобы он мог распространять содержимое на несколько клиентов?
  • Как вы будете обрабатывать сигнализацию/настройку сеанса RTP на RTSP-сервер? Вам необходимо как-то уведомить RTSP-сервер о том, что пользователь будет загружать живые медиа, чтобы он мог открыть соответствующие RTP/RTCP и т.д.
  • Как вы будете обрабатывать аутентификацию? Несколько клиентских устройств?

Мои исследования до сих пор состоят в том, что мне приходится записывать видео на запись в локальный сокет, а не в файл, потому что 3gp файлы, если они записаны в файл, не могут быть доступны, до завершения (когда видео остановлено и информация заголовка была записана в видео о длине и других).

Отправка кадров в режиме реального времени по протоколу RTP/RTCP - правильный подход. Когда устройство захвата захватывает каждый кадр, вам необходимо закодировать/сжать его и отправить его через сокет. 3gp, как и mp4, представляет собой формат контейнера, используемый для хранения файлов. Для записи в реальном времени нет необходимости писать в файл. Единственный раз, когда это имеет смысл, например, в потоках HTTP Live Streaming или DASH, где носители записываются в транспортный поток или файл mp4, перед тем как обслуживаться через HTTP.

Когда сокет получает непрерывные данные, мне нужно будет обернуть его в пакет RTP и отправить его на удаленный сервер. Возможно, мне также придется сначала выполнить базовую кодировку (что еще не так важно).

Я бы не согласился, кодирование очень важно, вы, вероятно, никогда не сможете отправить видео в противном случае, и вам придется иметь дело с такими проблемами, как стоимость (по мобильным сетям) и просто объем средств массовой информации в зависимости от разрешение и частота кадров.

Есть ли у кого-нибудь идеи, если эта теория правильна до сих пор. Я также хотел бы узнать, может ли кто-нибудь указать мне несколько фрагментов кода подобных подходов, особенно для отправки видео "на лету" на сервер. Я еще не уверен, как это сделать.

Взгляните на spydroid проект с открытым исходным кодом в качестве отправной точки. Он содержит множество необходимых шагов, включая настройку кодировщика, пакетный доступ к RTP, отправку RTCP, а также некоторые функции сервера RTSP. Spydroid устанавливает сервер RTSP, поэтому носитель кодируется и отправляется после того, как клиент RTSP, такой как VLC, используется для настройки сеанса RTSP. Поскольку ваше приложение управляется пользователем телефона, который хочет отправить медиа на сервер, возможно, вам придется рассмотреть другой подход к началу отправки, даже если вы отправляете какое-то сообщение на сервер, например, для установки сеанса RTSP, например, в spydroid.

Ответ 2

Год назад я создал приложение для Android, которое могло передавать его камеру/микрофон, используя rtsp через tcp на wowza media server.

Общий подход - создать unix-сокет, получить его файловый дескриптор и передать его в компонент рекордера Android. Затем медиамагнитофону поручают записывать видео в формате mp4/h264 в этот дескриптор файла. Теперь ваше приложение читает клиентский сокет, анализирует mp4, чтобы удалить заголовок и получить iframes из него, и введет его в поток rtsp на лету.

Нечто подобное может быть сделано и для звука (обычно AAC). Разумеется, вам приходится обрабатывать время, пробивающее ваше "я", а самая сложная вещь в целом - это синхронизация видео/аудио.

Итак, вот первая его часть. Что-то, что можно назвать rtspsocket. Он ведет переговоры с медиа-сервером в методе подключения, после чего вы можете записать в него сам поток. Я покажу его позже.

package com.example.android.streaming.streaming.rtsp;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Locale;
import java.util.concurrent.ConcurrentHashMap;

import android.util.Base64;
import android.util.Log;

import com.example.android.streaming.StreamingApp;
import com.example.android.streaming.streaming.Session;
import com.example.android.streaming.BuildConfig;

public class RtspSocket extends Socket {
    public static final int RTSP_HEADER_LENGTH = 4;
    public static final int RTP_HEADER_LENGTH = 12;
    public static final int MTU = 1400;

    public static final int PAYLOAD_OFFSET = RTSP_HEADER_LENGTH + RTP_HEADER_LENGTH;
    public static final int RTP_OFFSET = RTSP_HEADER_LENGTH;

    private ConcurrentHashMap<String, String> headerMap = new ConcurrentHashMap<String, String>();

    static private final String kCRLF = "\r\n";

    // RTSP request format strings
    static private final String kOptions = "OPTIONS %s RTSP/1.0\r\n";
    static private final String kDescribe = "DESCRIBE %s RTSP/1.0\r\n";
    static private final String kAnnounce = "ANNOUNCE %s RTSP/1.0\r\n";
    static private final String kSetupPublish = "SETUP %s/trackid=%d RTSP/1.0\r\n";
    @SuppressWarnings("unused")
    static private final String kSetupPlay = "SETUP %s/trackid=%d RTSP/1.0\r\n";
    static private final String kRecord = "RECORD %s RTSP/1.0\r\n";
    static private final String kPlay = "PLAY %s RTSP/1.0\r\n";
    static private final String kTeardown = "TEARDOWN %s RTSP/1.0\r\n";

    // RTSP header format strings
    static private final String kCseq = "Cseq: %d\r\n";
    static private final String kContentLength = "Content-Length: %d\r\n";
    static private final String kContentType = "Content-Type: %s\r\n";
    static private final String kTransport = "Transport: RTP/AVP/%s;unicast;mode=%s;%s\r\n";
    static private final String kSession = "Session: %s\r\n";
    static private final String kRange = "range: %s\r\n";
    static private final String kAccept = "Accept: %s\r\n";
    static private final String kAuthBasic = "Authorization: Basic %s\r\n";
    static private final String kAuthDigest = "Authorization: Digest username=\"%s\",realm=\"%s\",nonce=\"%s\",uri=\"%s\",response=\"%s\"\r\n";

    // RTSP header keys
    static private final String kSessionKey = "Session";
    static private final String kWWWAuthKey = "WWW-Authenticate";

    byte header[] = new byte[RTSP_MAX_HEADER + 1];
    static private final int RTSP_MAX_HEADER = 4095;
    static private final int RTSP_MAX_BODY = 4095;

    static private final int RTSP_RESP_ERR = -6;
    // static private final int RTSP_RESP_ERR_SESSION = -7;
    static public final int RTSP_OK = 200;
    static private final int RTSP_BAD_USER_PASS = 401;

    static private final int SOCK_ERR_READ = -5;

    /* Number of channels including control ones. */
    private int channelCount = 0;

    /* RTSP negotiation cmd seq counter */
    private int seq = 0;

    private String authentication = null;
    private String session = null;

    private String path = null;
    private String url = null;
    private String user = null;
    private String pass = null;
    private String sdp = null;

    private byte[] buffer = new byte[MTU];

    public RtspSocket() {
        super();
        try {
            setTcpNoDelay(true);
            setSoTimeout(60000);
        } catch (SocketException e) {
            Log.e(StreamingApp.TAG, "Failed to set socket params.");
        }
        buffer[RTSP_HEADER_LENGTH] = (byte) Integer.parseInt("10000000", 2);
    }

    public byte[] getBuffer() {
        return buffer;
    }

    public static final void setLong(byte[] buffer, long n, int begin, int end) {
        for (end--; end >= begin; end--) {
            buffer[end] = (byte) (n % 256);
            n >>= 8;
        }
    }

    public void setSequence(int seq) {
        setLong(buffer, seq, RTP_OFFSET + 2, RTP_OFFSET + 4);
    }

    public void setSSRC(int ssrc) {
        setLong(buffer, ssrc, RTP_OFFSET + 8, RTP_OFFSET + 12);
    }

    public void setPayload(int payload) {
        buffer[RTP_OFFSET + 1] = (byte) (payload & 0x7f);
    }

    public void setRtpTimestamp(long timestamp) {
        setLong(buffer, timestamp, RTP_OFFSET + 4, RTP_OFFSET + 8);
    }

    /** Sends the RTP packet over the network */
    private void send(int length, int stream) throws IOException {
        buffer[0] = '$';
        buffer[1] = (byte) stream;
        setLong(buffer, length, 2, 4);
        OutputStream s = getOutputStream();
        s.write(buffer, 0, length + RTSP_HEADER_LENGTH);
        s.flush();
    }

    public void sendReport(int length, int ssrc, int stream) throws IOException {
        setPayload(200);
        setLong(buffer, ssrc, RTP_OFFSET + 4, RTP_OFFSET + 8);
        send(length + RTP_HEADER_LENGTH, stream);
    }

    public void sendData(int length, int ssrc, int seq, int payload, int stream, boolean last) throws IOException {
        setSSRC(ssrc);
        setSequence(seq);
        setPayload(payload);
        buffer[RTP_OFFSET + 1] |= (((last ? 1 : 0) & 0x01) << 7);
        send(length + RTP_HEADER_LENGTH, stream);
    }

    public int getChannelCount() {
        return channelCount;
    }

    private void write(String request) throws IOException {
        try {
            String asci = new String(request.getBytes(), "US-ASCII");
            OutputStream out = getOutputStream();
            out.write(asci.getBytes());
        } catch (IOException e) {
            throw new IOException("Error writing to socket.");
        }
    }

    private String read() throws IOException {
        String response = null;
        try {
            InputStream in = getInputStream();
            int i = 0, len = 0, crlf_count = 0;
            boolean parsedHeader = false;

            for (; i < RTSP_MAX_BODY && !parsedHeader && len > -1; i++) {
                len = in.read(header, i, 1);
                if (header[i] == '\r' || header[i] == '\n') {
                    crlf_count++;
                    if (crlf_count == 4)
                        parsedHeader = true;
                } else {
                    crlf_count = 0;
                }
            }
            if (len != -1) {
                len = i;
                header[len] = '\0';
                response = new String(header, 0, len, "US-ASCII");
            }
        } catch (IOException e) {
            throw new IOException("Connection timed out. Check your network settings.");
        }
        return response;
    }

    private int parseResponse(String response) {
        String[] lines = response.split(kCRLF);
        String[] items = response.split(" ");
        String tempString, key, value;

        headerMap.clear();
        if (items.length < 2)
            return RTSP_RESP_ERR;
        int responseCode = RTSP_RESP_ERR;
        try {
            responseCode = Integer.parseInt(items[1]);
        } catch (Exception e) {
            Log.w(StreamingApp.TAG, e.getMessage());
            Log.w(StreamingApp.TAG, response);
        }
        if (responseCode == RTSP_RESP_ERR)
            return responseCode;

        // Parse response header into key value pairs.
        for (int i = 1; i < lines.length; i++) {
            tempString = lines[i];

            if (tempString.length() == 0)
                break;

            int idx = tempString.indexOf(":");

            if (idx == -1)
                continue;

            key = tempString.substring(0, idx);
            value = tempString.substring(idx + 1);
            headerMap.put(key, value);
        }

        tempString = headerMap.get(kSessionKey);
        if (tempString != null) {
            // Parse session
            items = tempString.split(";");
            tempString = items[0];
            session = tempString.trim();
        }

        return responseCode;
    }

    private void generateBasicAuth() throws UnsupportedEncodingException {
        String userpass = String.format("%s:%s", user, pass);
        authentication = String.format(kAuthBasic, Base64.encodeToString(userpass.getBytes("US-ASCII"), Base64.DEFAULT));
    }

    public static String md5(String s) {
        MessageDigest digest;
        try {
            digest = MessageDigest.getInstance("MD5");
            digest.update(s.getBytes(), 0, s.length());
            String hash = new BigInteger(1, digest.digest()).toString(16);
            return hash;
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return "";
    }

    static private final int CC_MD5_DIGEST_LENGTH = 16;

    private String md5HexDigest(String input) {
        byte digest[] = md5(input).getBytes();
        String result = new String();
        for (int i = 0; i < CC_MD5_DIGEST_LENGTH; i++)
            result = result.concat(String.format("%02x", digest[i]));
        return result;
    }

    private void generateDigestAuth(String method) {
        String nonce, realm;
        String ha1, ha2, response;

        // WWW-Authenticate: Digest realm="Streaming Server",
        // nonce="206351b944cb28fe37a0794848c2e36f"
        String wwwauth = headerMap.get(kWWWAuthKey);
        int idx = wwwauth.indexOf("Digest");
        String authReq = wwwauth.substring(idx + "Digest".length() + 1);

        if (BuildConfig.DEBUG)
            Log.d(StreamingApp.TAG, String.format("Auth Req: %s", authReq));

        String[] split = authReq.split(",");
        realm = split[0];
        nonce = split[1];

        split = realm.split("=");
        realm = split[1];
        realm = realm.substring(1, 1 + realm.length() - 2);

        split = nonce.split("=");
        nonce = split[1];
        nonce = nonce.substring(1, 1 + nonce.length() - 2);

        if (BuildConfig.DEBUG) {
            Log.d(StreamingApp.TAG, String.format("realm=%s", realm));
            Log.d(StreamingApp.TAG, String.format("nonce=%s", nonce));
        }

        ha1 = md5HexDigest(String.format("%s:%s:%s", user, realm, pass));
        ha2 = md5HexDigest(String.format("%s:%s", method, url));
        response = md5HexDigest(String.format("%s:%s:%s", ha1, nonce, ha2));
        authentication = md5HexDigest(String.format(kAuthDigest, user, realm, nonce, url, response));
    }

    private int options() throws IOException {
        seq++;
        StringBuilder request = new StringBuilder();
        request.append(String.format(kOptions, url));
        request.append(String.format(kCseq, seq));
        request.append(kCRLF);
        if (BuildConfig.DEBUG)
            Log.d(StreamingApp.TAG, "--- OPTIONS Request ---\n\n" + request);
        write(request.toString());
        String response = read();
        if (response == null)
            return SOCK_ERR_READ;
        if (BuildConfig.DEBUG)
            Log.d(StreamingApp.TAG, "--- OPTIONS Response ---\n\n" + response);
        return parseResponse(response);
    }

    @SuppressWarnings("unused")
    private int describe() throws IOException {
        seq++;
        StringBuilder request = new StringBuilder();
        request.append(String.format(kDescribe, url));
        request.append(String.format(kAccept, "application/sdp"));
        request.append(String.format(kCseq, seq));
        request.append(kCRLF);
        if (BuildConfig.DEBUG)
            Log.d(StreamingApp.TAG, "--- DESCRIBE Request ---\n\n" + request);
        write(request.toString());
        String response = read();
        if (response == null)
            return SOCK_ERR_READ;
        if (BuildConfig.DEBUG)
            Log.d(StreamingApp.TAG, "--- DESCRIBE Response ---\n\n" + response);
        return parseResponse(response);
    }

    private int recurseDepth = 0;

    private int announce() throws IOException {
        seq++;
        recurseDepth = 0;
        StringBuilder request = new StringBuilder();
        request.append(String.format(kAnnounce, url));
        request.append(String.format(kCseq, seq));
        request.append(String.format(kContentLength, sdp.length()));
        request.append(String.format(kContentType, "application/sdp"));
        request.append(kCRLF);
        if (sdp.length() > 0)
            request.append(sdp);
        if (BuildConfig.DEBUG)
            Log.d(StreamingApp.TAG, "--- ANNOUNCE Request ---\n\n" + request);
        write(request.toString());
        String response = read();
        if (response == null)
            return SOCK_ERR_READ;
        if (BuildConfig.DEBUG)
            Log.d(StreamingApp.TAG, "--- ANNOUNCE Response ---\n\n" + response);

        int ret = parseResponse(response);
        if (ret == RTSP_BAD_USER_PASS && recurseDepth == 0) {
            String wwwauth = headerMap.get(kWWWAuthKey);
            if (wwwauth != null) {
                if (BuildConfig.DEBUG)
                    Log.d(StreamingApp.TAG, String.format("WWW Auth Value: %s", wwwauth));
                int idx = wwwauth.indexOf("Basic");
                recurseDepth++;

                if (idx != -1) {
                    generateBasicAuth();
                } else {
                    // We are assuming Digest here.
                    generateDigestAuth("ANNOUNCE");
                }

                ret = announce();
                recurseDepth--;
            }
        }
        return ret;
    }

    private int setup(int trackId) throws IOException {
        seq++;
        recurseDepth = 0;
        StringBuilder request = new StringBuilder();
        request.append(String.format(kSetupPublish, url, trackId));
        request.append(String.format(kCseq, seq));

        /* One channel for rtp (data) and one for rtcp (control) */
        String tempString = String.format(Locale.getDefault(), "interleaved=%d-%d", channelCount++, channelCount++);

        request.append(String.format(kTransport, "TCP", "record", tempString));
        request.append(kCRLF);
        if (BuildConfig.DEBUG)
            Log.d(StreamingApp.TAG, "--- SETUP Request ---\n\n" + request);
        write(request.toString());
        String response = read();
        if (response == null)
            return SOCK_ERR_READ;
        if (BuildConfig.DEBUG)
            Log.d(StreamingApp.TAG, "--- SETUP Response ---\n\n" + response);

        int ret = parseResponse(response);
        if (ret == RTSP_BAD_USER_PASS && recurseDepth == 0) {
            String wwwauth = headerMap.get(kWWWAuthKey);
            if (wwwauth != null) {
                if (BuildConfig.DEBUG)
                    Log.d(StreamingApp.TAG, String.format("WWW Auth Value: %s", wwwauth));
                int idx = wwwauth.indexOf("Basic");
                recurseDepth++;

                if (idx != -1) {
                    generateBasicAuth();
                } else {
                    // We are assuming Digest here.
                    generateDigestAuth("SETUP");
                }

                ret = setup(trackId);
                authentication = null;
                recurseDepth--;
            }
        }
        return ret;
    }

    private int record() throws IOException {
        seq++;
        recurseDepth = 0;
        StringBuilder request = new StringBuilder();
        request.append(String.format(kRecord, url));
        request.append(String.format(kCseq, seq));
        request.append(String.format(kRange, "npt=0.000-"));
        if (authentication != null)
            request.append(authentication);
        if (session != null)
            request.append(String.format(kSession, session));
        request.append(kCRLF);
        if (BuildConfig.DEBUG)
            Log.d(StreamingApp.TAG, "--- RECORD Request ---\n\n" + request);
        write(request.toString());
        String response = read();
        if (response == null)
            return SOCK_ERR_READ;
        if (BuildConfig.DEBUG)
            Log.d(StreamingApp.TAG, "--- RECORD Response ---\n\n" + response);
        int ret = parseResponse(response);
        if (ret == RTSP_BAD_USER_PASS && recurseDepth == 0) {
            String wwwauth = headerMap.get(kWWWAuthKey);
            if (wwwauth != null) {
                if (BuildConfig.DEBUG)
                    Log.d(StreamingApp.TAG, String.format("WWW Auth Value: %s", wwwauth));
                int idx = wwwauth.indexOf("Basic");
                recurseDepth++;

                if (idx != -1) {
                    generateBasicAuth();
                } else {
                    // We are assuming Digest here.
                    generateDigestAuth("RECORD");
                }

                ret = record();
                authentication = null;
                recurseDepth--;
            }
        }
        return ret;
    }

    @SuppressWarnings("unused")
    private int play() throws IOException {
        seq++;
        recurseDepth = 0;
        StringBuilder request = new StringBuilder();
        request.append(String.format(kPlay, url));
        request.append(String.format(kCseq, seq));
        request.append(String.format(kRange, "npt=0.000-"));
        if (authentication != null)
            request.append(authentication);
        if (session != null)
            request.append(String.format(kSession, session));
        request.append(kCRLF);
        if (BuildConfig.DEBUG)
            Log.d(StreamingApp.TAG, "--- PLAY Request ---\n\n" + request);
        write(request.toString());
        String response = read();
        if (response == null)
            return SOCK_ERR_READ;
        if (BuildConfig.DEBUG)
            Log.d(StreamingApp.TAG, "--- PLAY Response ---\n\n" + response);
        int ret = parseResponse(response);
        if (ret == RTSP_BAD_USER_PASS && recurseDepth == 0) {
            String wwwauth = headerMap.get(kWWWAuthKey);
            if (wwwauth != null) {
                if (BuildConfig.DEBUG)
                    Log.d(StreamingApp.TAG, String.format("WWW Auth Value: %s", wwwauth));
                int idx = wwwauth.indexOf("Basic");
                recurseDepth++;

                if (idx != -1) {
                    generateBasicAuth();
                } else {
                    // We are assuming Digest here.
                    generateDigestAuth("PLAY");
                }

                ret = record();
                authentication = null;
                recurseDepth--;
            }
        }
        return ret;
    }

    private int teardown() throws IOException {
        seq++;
        recurseDepth = 0;
        StringBuilder request = new StringBuilder();
        request.append(String.format(kTeardown, url));
        request.append(String.format(kCseq, seq));
        if (authentication != null)
            request.append(authentication);
        if (session != null)
            request.append(String.format(kSession, session));
        request.append(kCRLF);
        if (BuildConfig.DEBUG)
            Log.d(StreamingApp.TAG, "--- TEARDOWN Request ---\n\n" + request);
        write(request.toString());
        String response = read();
        if (response == null)
            return SOCK_ERR_READ;
        if (BuildConfig.DEBUG)
            Log.d(StreamingApp.TAG, "--- TEARDOWN Response ---\n\n" + response);
        int ret = parseResponse(response);
        if (ret == RTSP_BAD_USER_PASS && recurseDepth == 0) {
            String wwwauth = headerMap.get(kWWWAuthKey);
            if (wwwauth != null) {
                if (BuildConfig.DEBUG)
                    Log.d(StreamingApp.TAG, String.format("WWW Auth Value: %s", wwwauth));
                int idx = wwwauth.indexOf("Basic");
                recurseDepth++;

                if (idx != -1) {
                    generateBasicAuth();
                } else {
                    // We are assuming Digest here.
                    generateDigestAuth("TEARDOWN");
                }

                ret = record();
                authentication = null;
                recurseDepth--;
            }
        }
        return ret;
    }

    public void connect(String dest, int port, Session session) throws IOException {
        int trackId = 1;
        int responseCode;

        if (isConnected())
            return;

        if (!session.hasAudioTrack() && !session.hasVideoTrack())
            throw new IOException("No tracks found in session.");

        InetSocketAddress addr = null;
        try {
            addr = new InetSocketAddress(dest, port);
        } catch (Exception e) {
            throw new IOException("Failed to resolve rtsp server address.");
        }

        this.sdp = session.getSDP();
        this.user = session.getUser();
        this.pass = session.getPass();
        this.path = session.getPath();
        this.url = String.format("rtsp://%s:%d%s", dest, addr.getPort(), this.path);

        try {
            super.connect(addr);
        } catch (IOException e) {
            throw new IOException("Failed to connect rtsp server.");
        }

        responseCode = announce();
        if (responseCode != RTSP_OK) {
            close();
            throw new IOException("RTSP announce failed: " + responseCode);
        }

        responseCode = options();
        if (responseCode != RTSP_OK) {
            close();
            throw new IOException("RTSP options failed: " + responseCode);
        }

        /* Setup audio */
        if (session.hasAudioTrack()) {
            session.getAudioTrack().setStreamId(channelCount);
            responseCode = setup(trackId++);
            if (responseCode != RTSP_OK) {
                close();
                throw new IOException("RTSP video failed: " + responseCode);
            }
        }

        /* Setup video */
        if (session.hasVideoTrack()) {
            session.getVideoTrack().setStreamId(channelCount);
            responseCode = setup(trackId++);
            if (responseCode != RTSP_OK) {
                close();
                throw new IOException("RTSP audio setup failed: " + responseCode);
            }
        }

        responseCode = record();
        if (responseCode != RTSP_OK) {
            close();
            throw new IOException("RTSP record failed: " + responseCode);
        }
    }

    public void close() throws IOException {
        if (!isConnected())
            return;
        teardown();
        super.close();
    }
}

Ответ 3

Я попытался добиться того же результата (но отказался из-за отсутствия опыта). Мой путь состоял в том, чтобы использовать ffmpeg и/или avlib, потому что у него уже есть рабочий стол rtmp. Таким образом, в теории все, что вам нужно, - это маршрут видеопотока к процессу ffmpeg, который будет передаваться на сервер.

Ответ 4

Есть ли причина использовать 3gp на стороне клиента? С mp4 (с атомом MOOV, установленным в заголовке) вы можете прочитать временный файл в кусках и отправить на сервер, скорее всего, будет небольшая временная задержка, все зависит от скорости вашего соединения. Ваш сервер rtsp должен иметь возможность повторно кодировать mp4 обратно на 3gp для просмотра с низкой пропускной способностью.

Ответ 5

В этот момент, если я должен был принять камеру (необработанный поток) и сразу сделать ее доступной для набора клиентов, я бы пошел по маршруту google goouts и использовал WebRTC. см. ondello "раздел платформы" для набора инструментов /SDK. Во время вашей оценки вы должны были рассмотреть сравнительные достоинства WebRTC v RTSP.

ИМО с ее состоянием, RTSP будет ночной позади брандмауэров и с NAT. AFAIK на 3G/4G использование RTP в сторонних приложениях немного рискованно.

Тем не менее, я положил на git старый проект android/rtp/rtsp/sdp используя libs из netty и 'efflux', Я думаю, что этот проект пытался извлечь и воспроизвести только звуковую дорожку в контейнере (vid track проигнорирован и не вытащил через сеть) из видео Youtube, все из которых были закодированы для RTSP в то время. Я думаю, что были некоторые проблемы с пакетом и заголовками кадров, и я устал от RTSP и уронил его.

Если вы должны преследовать RTP/RTSP, некоторые из материалов пакета и уровня кадра, о которых упоминались другие плакаты, находятся прямо в классах Android и в тестовых случаях, которые поставляются с efflux

Ответ 6

И вот класс rtsp session. Он использует сокет rtsp для разговора с медиа-сервером. Его целью является также сохранение параметров сеанса, таких как потоки, которые он может отправлять (видео и/или аудио), очереди, несколько аудио/видео-код синхронизации.

Используемый интерфейс.

package com.example.android.streaming.streaming.rtsp;

public interface PacketListener {
    public void onPacketReceived(Packet p);
}

Сам сеанс.

package com.example.android.streaming.streaming;

import static java.util.EnumSet.of;

import java.io.IOException;
import java.util.EnumSet;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

import android.app.Activity;
import android.content.SharedPreferences;
import android.hardware.Camera;
import android.hardware.Camera.CameraInfo;
import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.SurfaceHolder;

import com.example.android.streaming.BuildConfig;
import com.example.android.streaming.StreamingApp;
import com.example.android.streaming.streaming.audio.AACStream;
import com.example.android.streaming.streaming.rtsp.Packet;
import com.example.android.streaming.streaming.rtsp.Packet.PacketType;
import com.example.android.streaming.streaming.rtsp.PacketListener;
import com.example.android.streaming.streaming.rtsp.RtspSocket;
import com.example.android.streaming.streaming.video.H264Stream;
import com.example.android.streaming.streaming.video.VideoConfig;
import com.example.android.streaming.streaming.video.VideoStream;

public class Session implements PacketListener, Runnable {
    public final static int MESSAGE_START = 0x03;
    public final static int MESSAGE_STOP = 0x04;
    public final static int VIDEO_H264 = 0x01;
    public final static int AUDIO_AAC = 0x05;

    public final static int VIDEO_TRACK = 1;
    public final static int AUDIO_TRACK = 0;

    private static VideoConfig defaultVideoQuality = VideoConfig.defaultVideoQualiy.clone();
    private static int defaultVideoEncoder = VIDEO_H264, defaultAudioEncoder = AUDIO_AAC;

    private static Session sessionUsingTheCamera = null;
    private static Session sessionUsingTheCamcorder = null;

    private static int startedStreamCount = 0;

    private int sessionTrackCount = 0;

    private static SurfaceHolder surfaceHolder;
    private Stream[] streamList = new Stream[2];
    protected RtspSocket socket = null;
    private Activity context = null;

    private String host = null;
    private String path = null;
    private String user = null;
    private String pass = null;
    private int port;

    public interface SessionListener {
        public void startSession(Session session);

        public void stopSession(Session session);
    };

    public Session(Activity context, String host, int port, String path, String user, String pass) {
        this.context = context;
        this.host = host;
        this.port = port;
        this.path = path;
        this.pass = pass;
    }

    public boolean isConnected() {
        return socket != null && socket.isConnected();
    }

    /**
     * Connect to rtsp server and start new session. This should be called when
     * all the streams are added so that proper sdp can be generated.
     */
    public void connect() throws IOException {
        try {
            socket = new RtspSocket();
            socket.connect(host, port, this);
        } catch (IOException e) {
            socket = null;
            throw e;
        }
    }

    public void close() throws IOException {
        if (socket != null) {
            socket.close();
            socket = null;
        }
    }

    public static void setDefaultVideoQuality(VideoConfig quality) {
        defaultVideoQuality = quality;
    }

    public static void setDefaultAudioEncoder(int encoder) {
        defaultAudioEncoder = encoder;
    }

    public static void setDefaultVideoEncoder(int encoder) {
        defaultVideoEncoder = encoder;
    }

    public static void setSurfaceHolder(SurfaceHolder sh) {
        surfaceHolder = sh;
    }

    public boolean hasVideoTrack() {
        return getVideoTrack() != null;
    }

    public MediaStream getVideoTrack() {
        return (MediaStream) streamList[VIDEO_TRACK];
    }

    public void addVideoTrack(Camera camera, CameraInfo info) throws IllegalStateException, IOException {
        addVideoTrack(camera, info, defaultVideoEncoder, defaultVideoQuality, false);
    }

    public synchronized void addVideoTrack(Camera camera, CameraInfo info, int encoder, VideoConfig quality,
            boolean flash) throws IllegalStateException, IOException {
        if (isCameraInUse())
            throw new IllegalStateException("Camera already in use by another client.");
        Stream stream = null;
        VideoConfig.merge(quality, defaultVideoQuality);

        switch (encoder) {
        case VIDEO_H264:
            if (BuildConfig.DEBUG)
                Log.d(StreamingApp.TAG, "Video streaming: H.264");
            SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext());
            stream = new H264Stream(camera, info, this, prefs);
            break;
        }

        if (stream != null) {
            if (BuildConfig.DEBUG)
                Log.d(StreamingApp.TAG, "Quality is: " + quality.resX + "x" + quality.resY + "px " + quality.framerate
                        + "fps, " + quality.bitrate + "bps");
            ((VideoStream) stream).setVideoQuality(quality);
            ((VideoStream) stream).setPreviewDisplay(surfaceHolder.getSurface());
            streamList[VIDEO_TRACK] = stream;
            sessionUsingTheCamera = this;
            sessionTrackCount++;
        }
    }

    public boolean hasAudioTrack() {
        return getAudioTrack() != null;
    }

    public MediaStream getAudioTrack() {
        return (MediaStream) streamList[AUDIO_TRACK];
    }

    public void addAudioTrack() throws IOException {
        addAudioTrack(defaultAudioEncoder);
    }

    public synchronized void addAudioTrack(int encoder) throws IOException {
        if (sessionUsingTheCamcorder != null)
            throw new IllegalStateException("Audio device is already in use by another client.");
        Stream stream = null;

        switch (encoder) {
        case AUDIO_AAC:
            if (android.os.Build.VERSION.SDK_INT < 14)
                throw new IllegalStateException("This device does not support AAC.");
            if (BuildConfig.DEBUG)
                Log.d(StreamingApp.TAG, "Audio streaming: AAC");
            SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext());
            stream = new AACStream(this, prefs);
            break;
        }

        if (stream != null) {
            streamList[AUDIO_TRACK] = stream;
            sessionUsingTheCamcorder = this;
            sessionTrackCount++;
        }
    }

    public synchronized String getSDP() throws IllegalStateException, IOException {
        StringBuilder sdp = new StringBuilder();
        sdp.append("v=0\r\n");

        /*
         * The RFC 4566 (5.2) suggests to use an NTP timestamp here but we will
         * simply use a UNIX timestamp.
         */
        //sdp.append("o=- " + timestamp + " " + timestamp + " IN IP4 127.0.0.1\r\n");
        sdp.append("o=- 0 0 IN IP4 127.0.0.1\r\n");
        sdp.append("s=Vedroid\r\n");
        sdp.append("c=IN IP4 " + host + "\r\n");
        sdp.append("i=N/A\r\n");
        sdp.append("t=0 0\r\n");
        sdp.append("a=tool:Vedroid RTP\r\n");
        int payload = 96;
        int trackId = 1;
        for (int i = 0; i < streamList.length; i++) {
            if (streamList[i] != null) {
                streamList[i].setPayloadType(payload++);
                sdp.append(streamList[i].generateSDP());
                sdp.append("a=control:trackid=" + trackId++ + "\r\n");
            }
        }
        return sdp.toString();
    }

    public String getDest() {
        return host;
    }

    public int getTrackCount() {
        return sessionTrackCount;
    }

    public static boolean isCameraInUse() {
        return sessionUsingTheCamera != null;
    }

    /** Indicates whether or not the microphone is being used in a session. **/
    public static boolean isMicrophoneInUse() {
        return sessionUsingTheCamcorder != null;
    }

    private SessionListener listener = null;

    public synchronized void prepare(int trackId) throws IllegalStateException, IOException {
        Stream stream = streamList[trackId];
        if (stream != null && !stream.isStreaming())
            stream.prepare();
    }

    public synchronized void start(int trackId) throws IllegalStateException, IOException {
        Stream stream = streamList[trackId];
        if (stream != null && !stream.isStreaming()) {
            stream.start();
            if (BuildConfig.DEBUG)
                Log.d(StreamingApp.TAG, "Started " + (trackId == VIDEO_TRACK ? "video" : "audio") + " channel.");
            //            if (++startedStreamCount == 1 && listener != null)
            //                listener.startSession(this);
        }
    }

    public void startAll(SessionListener listener) throws IllegalStateException, IOException {
        this.listener = listener;
        startThread();

        for (int i = 0; i < streamList.length; i++)
            prepare(i);

        /*
         * Important to start video capture before audio capture. This makes
         * audio/video de-sync smaller.
         */
        for (int i = 0; i < streamList.length; i++)
            start(streamList.length - i - 1);
    }

    public synchronized void stopAll() {
        for (int i = 0; i < streamList.length; i++) {
            if (streamList[i] != null && streamList[i].isStreaming()) {
                streamList[i].stop();
                if (BuildConfig.DEBUG)
                    Log.d(StreamingApp.TAG, "Stopped " + (i == VIDEO_TRACK ? "video" : "audio") + " channel.");
                if (--startedStreamCount == 0 && listener != null)
                    listener.stopSession(this);
            }
        }
        stopThread();
        this.listener = null;
        if (BuildConfig.DEBUG)
            Log.d(StreamingApp.TAG, "Session stopped.");
    }

    public synchronized void flush() {
        for (int i = 0; i < streamList.length; i++) {
            if (streamList[i] != null) {
                streamList[i].release();
                if (i == VIDEO_TRACK)
                    sessionUsingTheCamera = null;
                else
                    sessionUsingTheCamcorder = null;
                streamList[i] = null;
            }
        }
    }

    public String getPath() {
        return path;
    }

    public String getUser() {
        return user;
    }

    public String getPass() {
        return pass;
    }

    private BlockingDeque<Packet> audioQueue = new LinkedBlockingDeque<Packet>(MAX_QUEUE_SIZE);
    private BlockingDeque<Packet> videoQueue = new LinkedBlockingDeque<Packet>(MAX_QUEUE_SIZE);
    private final static int MAX_QUEUE_SIZE = 1000;

    private void sendPacket(Packet p) {
        try {
            MediaStream channel = (p.type == PacketType.AudioPacketType ? getAudioTrack() : getVideoTrack());
            p.packetizer.send(p, socket, channel.getPayloadType(), channel.getStreamId());
            getPacketQueue(p.type).remove(p);
        } catch (IOException e) {
            Log.e(StreamingApp.TAG, "Failed to send packet: " + e.getMessage());
        }
    }

    private final ReentrantLock queueLock = new ReentrantLock();
    private final Condition morePackets = queueLock.newCondition();
    private AtomicBoolean stopped = new AtomicBoolean(true);
    private Thread t = null;

    private final void wakeupThread() {
        queueLock.lock();
        try {
            morePackets.signalAll();
        } finally {
            queueLock.unlock();
        }
    }

    public void startThread() {
        if (t == null) {
            t = new Thread(this);
            stopped.set(false);
            t.start();
        }
    }

    public void stopThread() {
        stopped.set(true);
        if (t != null) {
            t.interrupt();
            try {
                wakeupThread();
                t.join();
            } catch (InterruptedException e) {
            }
            t = null;
        }
        audioQueue.clear();
        videoQueue.clear();
    }

    private long getStreamEndSampleTimestamp(BlockingDeque<Packet> queue) {
        long sample = 0;
        try {
            sample = queue.getLast().getSampleTimestamp() + queue.getLast().getFrameLen();
        } catch (Exception e) {
        }
        return sample;
    }

    private PacketType syncType = PacketType.AnyPacketType;
    private boolean aligned = false;

    private final BlockingDeque<Packet> getPacketQueue(PacketType type) {
        return (type == PacketType.AudioPacketType ? audioQueue : videoQueue);
    }

    private void setPacketTimestamp(Packet p) {
        /* Don't sync on SEI packet. */
        if (!aligned && p.type != syncType) {
            long shift = getStreamEndSampleTimestamp(getPacketQueue(syncType));
            Log.w(StreamingApp.TAG, "Set shift +" + shift + "ms to "
                    + (p.type == PacketType.VideoPacketType ? "video" : "audio") + " stream ("
                    + (getPacketQueue(syncType).size() + 1) + ") packets.");
            p.setTimestamp(p.getDuration(shift));
            p.setSampleTimestamp(shift);
            if (listener != null)
                listener.startSession(this);
            aligned = true;
        } else {
            p.setTimestamp(p.packetizer.getTimestamp());
            p.setSampleTimestamp(p.packetizer.getSampleTimestamp());
        }

        p.packetizer.setSampleTimestamp(p.getSampleTimestamp() + p.getFrameLen());
        p.packetizer.setTimestamp(p.getTimestamp() + p.getDuration());

        //        if (BuildConfig.DEBUG) {
        //            Log.d(StreamingApp.TAG, (p.type == PacketType.VideoPacketType ? "Video" : "Audio") + " packet timestamp: "
        //                    + p.getTimestamp() + "; sampleTimestamp: " + p.getSampleTimestamp());
        //        }
    }

    /*
     * Drop first frames if len is less than this. First sync frame will have
     * frame len >= 10 ms.
     */
    private final static int MinimalSyncFrameLength = 15;

    @Override
    public void onPacketReceived(Packet p) {
        queueLock.lock();
        try {
            /*
             * We always synchronize on video stream. Some devices have video
             * coming faster than audio, this is ok. Audio stream time stamps
             * will be adjusted. Other devices that have audio come first will
             * see all audio packets dropped until first video packet comes.
             * Then upon first video packet we again adjust the audio stream by
             * time stamp of the last video packet in the queue.
             */
            if (syncType == PacketType.AnyPacketType && p.type == PacketType.VideoPacketType
                    && p.getFrameLen() >= MinimalSyncFrameLength)
                syncType = p.type;

            if (syncType == PacketType.VideoPacketType) {
                setPacketTimestamp(p);
                if (getPacketQueue(p.type).size() > MAX_QUEUE_SIZE - 1) {
                    Log.w(StreamingApp.TAG, "Queue (" + p.type + ") is full, dropping packet.");
                } else {
                    /*
                     * Wakeup sending thread only if channels synchronization is
                     * already done.
                     */
                    getPacketQueue(p.type).add(p);
                    if (aligned)
                        morePackets.signalAll();
                }
            }
        } finally {
            queueLock.unlock();
        }
    }

    private boolean hasMorePackets(EnumSet<Packet.PacketType> mask) {
        boolean gotPackets;

        if (mask.contains(PacketType.AudioPacketType) && mask.contains(PacketType.VideoPacketType)) {
            gotPackets = (audioQueue.size() > 0 && videoQueue.size() > 0) && aligned;
        } else {
            if (mask.contains(PacketType.AudioPacketType))
                gotPackets = (audioQueue.size() > 0);
            else if (mask.contains(PacketType.VideoPacketType))
                gotPackets = (videoQueue.size() > 0);
            else
                gotPackets = (videoQueue.size() > 0 || audioQueue.size() > 0);
        }
        return gotPackets;
    }

    private void waitPackets(EnumSet<Packet.PacketType> mask) {
        queueLock.lock();
        try {
            do {
                if (!stopped.get() && !hasMorePackets(mask)) {
                    try {
                        morePackets.await();
                    } catch (InterruptedException e) {
                    }
                }
            } while (!stopped.get() && !hasMorePackets(mask));
        } finally {
            queueLock.unlock();
        }
    }

    private void sendPackets() {
        boolean send;
        Packet a, v;

        /*
         * Wait for any type of packet and send asap. With time stamps correctly
         * set, the real send moment is not important and may be quite
         * different. Media server will only check for time stamps.
         */
        waitPackets(of(PacketType.AnyPacketType));

        v = videoQueue.peek();
        if (v != null) {
            sendPacket(v);

            do {
                a = audioQueue.peek();
                if ((send = (a != null && a.getSampleTimestamp() <= v.getSampleTimestamp())))
                    sendPacket(a);
            } while (!stopped.get() && send);
        } else {
            a = audioQueue.peek();
            if (a != null)
                sendPacket(a);
        }
    }

    @Override
    public void run() {
        Log.w(StreamingApp.TAG, "Session thread started.");

        /*
         * Wait for both types of front packets to come and synchronize on each
         * other.
         */
        waitPackets(of(PacketType.AudioPacketType, PacketType.VideoPacketType));

        while (!stopped.get())
            sendPackets();

        Log.w(StreamingApp.TAG, "Flushing session queues.");
        Log.w(StreamingApp.TAG, "    " + audioQueue.size() + " audio packets.");
        Log.w(StreamingApp.TAG, "    " + videoQueue.size() + " video packets.");

        long start = SystemClock.elapsedRealtime();
        while (audioQueue.size() > 0 || videoQueue.size() > 0)
            sendPackets();

        Log.w(StreamingApp.TAG, "Session thread stopped.");
        Log.w(StreamingApp.TAG, "Queues flush took " + (SystemClock.elapsedRealtime() - start) + " ms.");
    }
}

Ответ 7

Проверьте этот ответ: потоковое видео через WIFI?

Затем, если вы хотите увидеть прямую трансляцию в Android-телефоне, включите vlc-плагин внутри своего приложения и подключитесь через протокол потоковой передачи в реальном времени (rtsp).

Intent i = new Intent("org.videolan.vlc.VLCApplication.gui.video.VideoPlayerActivity");
i.setAction(Intent.ACTION_VIEW);
i.setData(Uri.parse("rtsp://10.0.0.179:8086/")); 
startActivity(i);

Если вы установили VLC на свой Android-телефон, вы можете передавать с использованием намерения и передавать IP-адрес и порт, как показано выше.