项目作者: octiplex

项目描述 :
Implementation of the RTMP protocol to broadcast video and audio on Android in pure Java
高级语言: Java
项目地址: git://github.com/octiplex/Android-RTMP-Muxer.git
创建时间: 2016-11-07T14:14:41Z
项目社区:https://github.com/octiplex/Android-RTMP-Muxer

开源协议:Other

下载


RTMP Java Muxer for Android

This project implements the RTMP protocol to broadcast video and audio TO (and only TO!) an RTMP server from Android using pure Java (no native extension).

It has been tested with Android MediaCodec encoder to send H264 (avc) video and with Android MediaRecorder to send AAC audio via RTMP to a server.

RFCs

This implementation uses the RTMP 3 protocol version: RTMP 3

It also uses the AMF0 file format: AMF0

Android dependency

This muxer has been developed to be used on Android but it actually contains only 1 dependency to the Android platform: the support-v4 library.

It’s used for annotations and also ArrayMap but if you want to use this library outside of Android you can remove the dependency quite easily.

Another dependency are logs which are made with the Log util of Android, but it’s also easily replaceable.

Contributions

This code is a small part of an internal project and is available “as is” without any guarantee. We are not providing implementation details on how to extract H264 video and AAC audio frames on purpose, this implementation is up to you.

If you want to contribute, feel free to ask question or make pull requests, we’ll be happy to review them :)

How to use

Runtime

To use the Muxer, you must respect the following runtime:

1- Start streaming by calling the start and createStream method

  1. public class MainActivity extends Activity implements RtmpMuxer.RtmpConnectionListener
  2. {
  3. private RtmpMuxer muxer;
  4. private void initMuxer()
  5. {
  6. muxer = new RtmpMuxer(host, port, new Time()
  7. {
  8. @Override
  9. public long getCurrentTimestamp()
  10. {
  11. return System.currentTimeMillis();
  12. }
  13. });
  14. // Always call start method from a background thread.
  15. new AsyncTask<Void, Void, Void>()
  16. {
  17. @Override
  18. protected Void doInBackground(Void... params)
  19. {
  20. muxer.start(this, "app", null, null);
  21. return null;
  22. }
  23. }.execute();
  24. }
  25. @Override
  26. public void onConnected()
  27. {
  28. // Muxer is connected to the RTMP server, you can create a stream to publish data
  29. new AsyncTask<Void, Void, Void>()
  30. {
  31. @Override
  32. protected Void doInBackground(Void... params)
  33. {
  34. muxer.createStream("playPath");
  35. return null;
  36. }
  37. }.execute();
  38. }
  39. @Override
  40. public void onReadyToPublish()
  41. {
  42. // Muxer is connected to the server and ready to receive data
  43. }
  44. @Override
  45. public void onConnectionError(IOException e)
  46. {
  47. // Error while connecting to the server
  48. }
  49. }

Note that this sample uses an AsyncTask to demonstrate threading but you should use another most efficient threading method into your app.

2- Send data to the Muxer using postVideo and postAudio (see next part for more info)

3- Stop streaming by calling the deleteStream method

4- Disconnect by calling the stop method

Sample of video posting with MediaCodec API

Once the RtmpMuxer is ready to publish, you can provide it with video data. Those data can be extracted from the MediaCodec encoder (other ways asn’t been tested). Here’s how to create an H264VideoFrame object from the MediaCodec buffer (extracted from the Grafika sample):

  1. public class VideoRecordingActivity extends Activity
  2. {
  3. private MediaCodec mEncoder;
  4. private MediaCodec.BufferInfo mBufferInfo;
  5. private RtmpMuxer muxer; // Already started muxer
  6. /**
  7. * Extracts all pending data from the encoder and forwards it to the muxer.
  8. */
  9. public void drainEncoder()
  10. {
  11. final int TIMEOUT_USEC = 10000;
  12. ByteBuffer[] encoderOutputBuffers = mEncoder.getOutputBuffers();
  13. while (true)
  14. {
  15. int encoderStatus = mEncoder.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
  16. if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED)
  17. {
  18. // not expected for an encoder
  19. encoderOutputBuffers = mEncoder.getOutputBuffers();
  20. }
  21. else
  22. {
  23. if (mBufferInfo.size != 0)
  24. {
  25. final boolean isHeader = (mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0;
  26. final boolean isKeyframe = (mBufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0 && !isHeader;
  27. final long timestamp = mBufferInfo.presentationTimeUs;
  28. final ByteBuffer encodedData = encoderOutputBuffers[encoderStatus];
  29. encodedData.position(mBufferInfo.offset);
  30. encodedData.limit(mBufferInfo.offset + mBufferInfo.size);
  31. // Extract video data
  32. final byte[] b = new byte[mBufferInfo.size];
  33. encodedData.get(b);
  34. // Always call postVideo method from a background thread.
  35. new AsyncTask<Void, Void, Void>()
  36. {
  37. @Override
  38. protected Void doInBackground(Void... params)
  39. {
  40. try
  41. {
  42. muxer.postVideo(new H264VideoFrame()
  43. {
  44. @Override
  45. public boolean isHeader()
  46. {
  47. return isHeader;
  48. }
  49. @Override
  50. public long getTimestamp()
  51. {
  52. return timestamp;
  53. }
  54. @NonNull
  55. @Override
  56. public byte[] getData()
  57. {
  58. return b;
  59. }
  60. @Override
  61. public boolean isKeyframe()
  62. {
  63. return isKeyframe;
  64. }
  65. });
  66. }
  67. catch(IOException e)
  68. {
  69. // An error occured while sending the video frame to the server
  70. }
  71. return null;
  72. }
  73. }.execute();
  74. }
  75. mEncoder.releaseOutputBuffer(encoderStatus, false);
  76. if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0)
  77. {
  78. break; // out of while
  79. }
  80. }
  81. }
  82. }
  83. }

Note that this sample is incomplete and you’ll have to manage encoder lifecycle. Also, avoid using AsyncTask for posting data since it will happen multiple times per seconds, it’s done here just as a sample.

Sample of audio posting with MediaRecorder API

Once the RtmpMuxer is ready to publish, you can also provide it with audio data. Those data can be extracted from the MediaRecorder (other ways asn’t been tested). Here’s how to create configure MediaRecorder to generate the right stream:

  1. public class AudioRecordingActivity extends Activity implements Runnable
  2. {
  3. /**
  4. * Instance of media recorder used to record audio
  5. */
  6. private MediaRecorder mediaRecorder;
  7. /**
  8. * File descriptors used to extract data from the {@link #mediaRecorder}
  9. */
  10. private ParcelFileDescriptor[] fileDescriptors;
  11. /**
  12. * Thread that will handle aac parsing using {@link #fileDescriptors} output
  13. */
  14. private Thread aacParsingThread;
  15. /**
  16. * Has AAC header been send yet
  17. */
  18. private boolean headerSent;
  19. /**
  20. * Already started muxer.
  21. */
  22. private RtmpMuxer muxer;
  23. public void configure() throws IOException
  24. {
  25. fileDescriptors = ParcelFileDescriptor.createPipe();
  26. aacParsingThread = new Thread(this);
  27. headerSent = false;
  28. mediaRecorder = new MediaRecorder();
  29. mediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER); // If you want to use the camera's microphone
  30. mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.AAC_ADTS);
  31. mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
  32. mediaRecorder.setOutputFile(fileDescriptors[1].getFileDescriptor());
  33. mediaRecorder.prepare();
  34. }
  35. public void startAudio()
  36. {
  37. mediaRecorder.start();
  38. aacParsingThread.start();
  39. }
  40. @Override
  41. public void run()
  42. {
  43. FileInputStream is = null;
  44. try
  45. {
  46. is = new FileInputStream(fileDescriptors[0].getFileDescriptor());
  47. while (true)
  48. {
  49. // TODO parse AAC: This sample doesn't provide AAC extracting complete method since it's not the purpose of this repository.
  50. if( !headerSent )
  51. {
  52. // TODO extract header data
  53. byte[] aacHeader;
  54. int numberOfChannel;
  55. int sampleSizeIndex;
  56. muxer.setAudioHeader(new AACAudioHeader()
  57. {
  58. @NonNull
  59. @Override
  60. public byte[] getData()
  61. {
  62. return aacHeader;
  63. }
  64. @Override
  65. public int getNumberOfChannels()
  66. {
  67. return numberOfChannel;
  68. }
  69. @Override
  70. public int getSampleSizeIndex()
  71. {
  72. return sampleSizeIndex;
  73. }
  74. });
  75. headerSent = true;
  76. }
  77. // TODO extract frame data
  78. final byte[] aacData;
  79. final long timestamp;
  80. // Don't call postAudio from the extracting thread.
  81. new AsyncTask<Void, Void, Void>()
  82. {
  83. @Override
  84. protected Void doInBackground(Void... params)
  85. {
  86. try
  87. {
  88. muxer.postAudio(new AACAudioFrame()
  89. {
  90. @Override
  91. public long getTimestamp()
  92. {
  93. return timestamp;
  94. }
  95. @NonNull
  96. @Override
  97. public byte[] getData()
  98. {
  99. return aacData;
  100. }
  101. });
  102. }
  103. catch(IOException e)
  104. {
  105. // An error occured while sending the audio frame to the server
  106. }
  107. return null;
  108. }
  109. }.execute();
  110. }
  111. }
  112. catch (Exception e)
  113. {
  114. // TODO handle error
  115. }
  116. finally
  117. {
  118. if( is != null )
  119. {
  120. try
  121. {
  122. is.close();
  123. }
  124. catch (Exception ignored){}
  125. }
  126. }
  127. }
  128. }

Note that this sample is incomplete and you’ll have to manage thread completion, AAC parsing and data extracting. Also, avoid using AsyncTask for posting data since it will happen multiple times per seconds, it’s done here just as a sample.

Authors

Licence

Code is available under the Revised BSD License, see LICENCE for more info.

  1. Copyright (c) 2016 Octiplex.
  2. All rights reserved.
  3. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
  4. * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
  5. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
  6. * Neither the names of Octiplex and Newzulu nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.