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

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

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

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

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

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

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

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

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

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

  • Я предполагаю, что вы хотите загрузить на сервер RTSP, чтобы он мог распространять содержимое на несколько клиентов?
  • Как вы будете обрабатывать сигнализацию / настройку сеанса RTP на сервере RTSP? Вам необходимо как-то уведомить RTSP-сервер о том, что пользователь загрузит live-медиа, чтобы он мог открыть соответствующие сокеты 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 ,

Год назад я создал приложение для 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(); } } 

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

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

На данный момент, если я должен был принять камеру (необработанный поток) и сразу сделать ее доступной для набора клиентов, я бы пошел по маршруту google goouts и использовал WebRTC. См. Ondello 'platform section' для набора инструментов / 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

И вот класс сессии rtsp. Он использует сокет 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."); } } 

Проверьте этот ответ: потоковое видео через 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-адрес и порт, как показано выше.