diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f95f9c1d..b4389e1b 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -99,6 +99,14 @@
android:supportsRtl="true"
android:theme="@style/Theme.MicroPhoto"
tools:targetApi="28">
+
+
+
+
+#include
+#include
+#include
+
+extern "C" {
+#include "rtmp/rtmpsuck.h"
+}
+
+extern STREAMING_SERVER *rtmpServer;
+
+extern "C"
+JNIEXPORT jlong JNICALL
+Java_com_xypower_mpapp_RtmpService_startService(JNIEnv *env, jobject thiz) {
+ // TODO: implement startService()
+ RtmpSuckMain(0);
+ return (jlong)rtmpServer;
+}
+
+extern "C"
+JNIEXPORT void JNICALL
+Java_com_xypower_mpapp_RtmpService_stopService(JNIEnv *env, jobject thiz, jlong native_handle) {
+ stopStreaming(rtmpServer);
+ // free(rtmpServer);
+
+}
diff --git a/app/src/main/cpp/rtmp/rtmpsuck.c b/app/src/main/cpp/rtmp/rtmpsuck.c
new file mode 100644
index 00000000..05443e8f
--- /dev/null
+++ b/app/src/main/cpp/rtmp/rtmpsuck.c
@@ -0,0 +1,1159 @@
+/* RTMP Proxy Server
+ * Copyright (C) 2009 Andrej Stepanchuk
+ * Copyright (C) 2009 Howard Chu
+ *
+ * This Program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * This Program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with RTMPDump; see the file COPYING. If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ */
+
+/* This is a Proxy Server that displays the connection parameters from a
+ * client and then saves any data streamed to the client.
+ */
+
+#include
+#include
+#include
+#include
+
+#include
+#include
+
+#include
+
+#include "thread.h"
+
+#include "rtmpsuck.h"
+
+#ifdef linux
+#include
+#endif
+
+#define RD_SUCCESS 0
+#define RD_FAILED 1
+#define RD_INCOMPLETE 2
+
+#define PACKET_SIZE 1024*1024
+
+#ifdef WIN32
+#define InitSockets() {\
+ WORD version; \
+ WSADATA wsaData; \
+ \
+ version = MAKEWORD(1,1); \
+ WSAStartup(version, &wsaData); }
+
+#define CleanupSockets() WSACleanup()
+#else
+#define InitSockets()
+#define CleanupSockets()
+#endif
+
+STREAMING_SERVER *rtmpServer = 0; // server structure pointer
+
+#define STR2AVAL(av,str) av.av_val = str; av.av_len = strlen(av.av_val)
+
+#ifdef _DEBUG
+uint32_t debugTS = 0;
+
+int pnum = 0;
+
+FILE *netstackdump = NULL;
+FILE *netstackdump_read = NULL;
+#endif
+
+#define BUFFERTIME (4*60*60*1000) /* 4 hours */
+
+#define SAVC(x) static const AVal av_##x = AVC(#x)
+
+SAVC(app);
+SAVC(connect);
+SAVC(flashVer);
+SAVC(swfUrl);
+SAVC(pageUrl);
+SAVC(tcUrl);
+SAVC(fpad);
+SAVC(capabilities);
+SAVC(audioCodecs);
+SAVC(videoCodecs);
+SAVC(videoFunction);
+SAVC(objectEncoding);
+SAVC(_result);
+SAVC(createStream);
+SAVC(play);
+SAVC(closeStream);
+SAVC(fmsVer);
+SAVC(mode);
+SAVC(level);
+SAVC(code);
+SAVC(secureToken);
+SAVC(onStatus);
+SAVC(close);
+static const AVal av_NetStream_Failed = AVC("NetStream.Failed");
+static const AVal av_NetStream_Play_Failed = AVC("NetStream.Play.Failed");
+static const AVal av_NetStream_Play_StreamNotFound =
+AVC("NetStream.Play.StreamNotFound");
+static const AVal av_NetConnection_Connect_InvalidApp =
+AVC("NetConnection.Connect.InvalidApp");
+static const AVal av_NetStream_Play_Start = AVC("NetStream.Play.Start");
+static const AVal av_NetStream_Play_Complete = AVC("NetStream.Play.Complete");
+static const AVal av_NetStream_Play_Stop = AVC("NetStream.Play.Stop");
+
+static const char *cst[] = { "client", "server" };
+
+// Returns 0 for OK/Failed/error, 1 for 'Stop or Complete'
+int
+ServeInvoke(STREAMING_SERVER *server, int which, RTMPPacket *pack, const char *body)
+{
+ int ret = 0, nRes;
+ int nBodySize = pack->m_nBodySize;
+
+ if (body > pack->m_body)
+ nBodySize--;
+
+ if (body[0] != 0x02) // make sure it is a string method name we start with
+ {
+ RTMP_Log(RTMP_LOGWARNING, "%s, Sanity failed. no string method in invoke packet",
+ __FUNCTION__);
+ return 0;
+ }
+
+ AMFObject obj;
+ nRes = AMF_Decode(&obj, body, nBodySize, FALSE);
+ if (nRes < 0)
+ {
+ RTMP_Log(RTMP_LOGERROR, "%s, error decoding invoke packet", __FUNCTION__);
+ return 0;
+ }
+
+ AMF_Dump(&obj);
+ AVal method;
+ AMFProp_GetString(AMF_GetProp(&obj, NULL, 0), &method);
+ RTMP_Log(RTMP_LOGDEBUG, "%s, %s invoking <%s>", __FUNCTION__, cst[which], method.av_val);
+
+ if (AVMATCH(&method, &av_connect))
+ {
+ AMFObject cobj;
+ AVal pname, pval;
+ int i;
+ AMFProp_GetObject(AMF_GetProp(&obj, NULL, 2), &cobj);
+ RTMP_LogPrintf("Processing connect\n");
+ for (i=0; irc.Link.app = pval;
+ pval.av_val = NULL;
+ }
+ else if (AVMATCH(&pname, &av_flashVer))
+ {
+ server->rc.Link.flashVer = pval;
+ pval.av_val = NULL;
+ }
+ else if (AVMATCH(&pname, &av_swfUrl))
+ {
+#ifdef CRYPTO
+ if (pval.av_val)
+ RTMP_HashSWF(pval.av_val, &server->rc.Link.SWFSize,
+ (unsigned char *)server->rc.Link.SWFHash, 30);
+#endif
+ server->rc.Link.swfUrl = pval;
+ pval.av_val = NULL;
+ }
+ else if (AVMATCH(&pname, &av_tcUrl))
+ {
+ char *r1 = NULL, *r2;
+ int len;
+
+ server->rc.Link.tcUrl = pval;
+ if ((pval.av_val[0] | 0x40) == 'r' &&
+ (pval.av_val[1] | 0x40) == 't' &&
+ (pval.av_val[2] | 0x40) == 'm' &&
+ (pval.av_val[3] | 0x40) == 'p')
+ {
+ if (pval.av_val[4] == ':')
+ {
+ server->rc.Link.protocol = RTMP_PROTOCOL_RTMP;
+ r1 = pval.av_val+7;
+ }
+ else if ((pval.av_val[4] | 0x40) == 'e' && pval.av_val[5] == ':')
+ {
+ server->rc.Link.protocol = RTMP_PROTOCOL_RTMPE;
+ r1 = pval.av_val+8;
+ }
+ r2 = strchr(r1, '/');
+ if (r2)
+ len = r2 - r1;
+ else
+ len = pval.av_len - (r1 - pval.av_val);
+ r2 = malloc(len+1);
+ memcpy(r2, r1, len);
+ r2[len] = '\0';
+ server->rc.Link.hostname.av_val = r2;
+ r1 = strrchr(r2, ':');
+ if (r1)
+ {
+ server->rc.Link.hostname.av_len = r1 - r2;
+ *r1++ = '\0';
+ server->rc.Link.port = atoi(r1);
+ }
+ else
+ {
+ server->rc.Link.hostname.av_len = len;
+ server->rc.Link.port = 1935;
+ }
+ }
+ pval.av_val = NULL;
+ }
+ else if (AVMATCH(&pname, &av_pageUrl))
+ {
+ server->rc.Link.pageUrl = pval;
+ pval.av_val = NULL;
+ }
+ else if (AVMATCH(&pname, &av_audioCodecs))
+ {
+ server->rc.m_fAudioCodecs = cobj.o_props[i].p_vu.p_number;
+ }
+ else if (AVMATCH(&pname, &av_videoCodecs))
+ {
+ server->rc.m_fVideoCodecs = cobj.o_props[i].p_vu.p_number;
+ }
+ else if (AVMATCH(&pname, &av_objectEncoding))
+ {
+ server->rc.m_fEncoding = cobj.o_props[i].p_vu.p_number;
+ server->rc.m_bSendEncoding = TRUE;
+ }
+ /* Dup'd a string we didn't recognize? */
+ if (pval.av_val)
+ free(pval.av_val);
+ }
+ if (obj.o_num > 3)
+ {
+ if (AMFProp_GetBoolean(&obj.o_props[3]))
+ server->rc.Link.lFlags |= RTMP_LF_AUTH;
+ if (obj.o_num > 4)
+ {
+ AMFProp_GetString(&obj.o_props[4], &server->rc.Link.auth);
+ }
+ }
+
+ if (!RTMP_Connect(&server->rc, pack))
+ {
+ /* failed */
+ return 1;
+ }
+ server->rc.m_bSendCounter = FALSE;
+ }
+ else if (AVMATCH(&method, &av_play))
+ {
+ Flist *fl;
+ AVal av;
+ FILE *out;
+ char *file, *p, *q;
+ char flvHeader[] = { 'F', 'L', 'V', 0x01,
+ 0x05, // video + audio, we finalize later if the value is different
+ 0x00, 0x00, 0x00, 0x09,
+ 0x00, 0x00, 0x00, 0x00 // first prevTagSize=0
+ };
+ int count = 0, flen;
+
+ server->rc.m_stream_id = pack->m_nInfoField2;
+ AMFProp_GetString(AMF_GetProp(&obj, NULL, 3), &av);
+ server->rc.Link.playpath = av;
+ if (!av.av_val)
+ goto out;
+
+ /* check for duplicates */
+ for (fl = server->f_head; fl; fl=fl->f_next)
+ {
+ if (AVMATCH(&av, &fl->f_path))
+ count++;
+ }
+ /* strip trailing URL parameters */
+ q = memchr(av.av_val, '?', av.av_len);
+ if (q)
+ {
+ if (q == av.av_val)
+ {
+ av.av_val++;
+ av.av_len--;
+ }
+ else
+ {
+ av.av_len = q - av.av_val;
+ }
+ }
+ /* strip leading slash components */
+ for (p=av.av_val+av.av_len-1; p>=av.av_val; p--)
+ if (*p == '/')
+ {
+ p++;
+ av.av_len -= p - av.av_val;
+ av.av_val = p;
+ break;
+ }
+ /* skip leading dot */
+ if (av.av_val[0] == '.')
+ {
+ av.av_val++;
+ av.av_len--;
+ }
+ flen = av.av_len;
+ /* hope there aren't more than 255 dups */
+ if (count)
+ flen += 2;
+ file = malloc(flen+1);
+
+ memcpy(file, av.av_val, av.av_len);
+ if (count)
+ sprintf(file+av.av_len, "%02x", count);
+ else
+ file[av.av_len] = '\0';
+ for (p=file; *p; p++)
+ if (*p == ':')
+ *p = '_';
+ RTMP_LogPrintf("Playpath: %.*s\nSaving as: %s\n",
+ server->rc.Link.playpath.av_len, server->rc.Link.playpath.av_val,
+ file);
+ out = fopen(file, "wb");
+ free(file);
+ if (!out)
+ ret = 1;
+ else
+ {
+ fwrite(flvHeader, 1, sizeof(flvHeader), out);
+ av = server->rc.Link.playpath;
+ fl = malloc(sizeof(Flist)+av.av_len+1);
+ fl->f_file = out;
+ fl->f_path.av_len = av.av_len;
+ fl->f_path.av_val = (char *)(fl+1);
+ memcpy(fl->f_path.av_val, av.av_val, av.av_len);
+ fl->f_path.av_val[av.av_len] = '\0';
+ fl->f_next = NULL;
+ if (server->f_tail)
+ server->f_tail->f_next = fl;
+ else
+ server->f_head = fl;
+ server->f_tail = fl;
+ }
+ }
+ else if (AVMATCH(&method, &av_onStatus))
+ {
+ AMFObject obj2;
+ AVal code, level;
+ AMFProp_GetObject(AMF_GetProp(&obj, NULL, 3), &obj2);
+ AMFProp_GetString(AMF_GetProp(&obj2, &av_code, -1), &code);
+ AMFProp_GetString(AMF_GetProp(&obj2, &av_level, -1), &level);
+
+ RTMP_Log(RTMP_LOGDEBUG, "%s, onStatus: %s", __FUNCTION__, code.av_val);
+ if (AVMATCH(&code, &av_NetStream_Failed)
+ || AVMATCH(&code, &av_NetStream_Play_Failed)
+ || AVMATCH(&code, &av_NetStream_Play_StreamNotFound)
+ || AVMATCH(&code, &av_NetConnection_Connect_InvalidApp))
+ {
+ ret = 1;
+ }
+
+ if (AVMATCH(&code, &av_NetStream_Play_Start))
+ {
+ /* set up the next stream */
+ if (server->f_cur)
+ {
+ if (server->f_cur->f_next)
+ server->f_cur = server->f_cur->f_next;
+ }
+ else
+ {
+ for (server->f_cur = server->f_head; server->f_cur &&
+ !server->f_cur->f_file; server->f_cur = server->f_cur->f_next) ;
+ }
+ server->rc.m_bPlaying = TRUE;
+ }
+
+ // Return 1 if this is a Play.Complete or Play.Stop
+ if (AVMATCH(&code, &av_NetStream_Play_Complete)
+ || AVMATCH(&code, &av_NetStream_Play_Stop))
+ {
+ ret = 1;
+ }
+ }
+ else if (AVMATCH(&method, &av_closeStream))
+ {
+ ret = 1;
+ }
+ else if (AVMATCH(&method, &av_close))
+ {
+ RTMP_Close(&server->rc);
+ ret = 1;
+ }
+out:
+ AMF_Reset(&obj);
+ return ret;
+}
+
+int
+ServePacket(STREAMING_SERVER *server, int which, RTMPPacket *packet)
+{
+ int ret = 0;
+
+ RTMP_Log(RTMP_LOGDEBUG, "%s, %s sent packet type %02X, size %u bytes", __FUNCTION__,
+ cst[which], packet->m_packetType, packet->m_nBodySize);
+
+ switch (packet->m_packetType)
+ {
+ case RTMP_PACKET_TYPE_CHUNK_SIZE:
+ // chunk size
+// HandleChangeChunkSize(r, packet);
+ break;
+
+ case RTMP_PACKET_TYPE_BYTES_READ_REPORT:
+ // bytes read report
+ break;
+
+ case RTMP_PACKET_TYPE_CONTROL:
+ // ctrl
+// HandleCtrl(r, packet);
+ break;
+
+ case RTMP_PACKET_TYPE_SERVER_BW:
+ // server bw
+// HandleServerBW(r, packet);
+ break;
+
+ case RTMP_PACKET_TYPE_CLIENT_BW:
+ // client bw
+ // HandleClientBW(r, packet);
+ break;
+
+ case RTMP_PACKET_TYPE_AUDIO:
+ // audio data
+ //RTMP_Log(RTMP_LOGDEBUG, "%s, received: audio %lu bytes", __FUNCTION__, packet.m_nBodySize);
+ break;
+
+ case RTMP_PACKET_TYPE_VIDEO:
+ // video data
+ //RTMP_Log(RTMP_LOGDEBUG, "%s, received: video %lu bytes", __FUNCTION__, packet.m_nBodySize);
+ break;
+
+ case RTMP_PACKET_TYPE_FLEX_STREAM_SEND:
+ // flex stream send
+ break;
+
+ case RTMP_PACKET_TYPE_FLEX_SHARED_OBJECT:
+ // flex shared object
+ break;
+
+ case RTMP_PACKET_TYPE_FLEX_MESSAGE:
+ // flex message
+ {
+ ret = ServeInvoke(server, which, packet, packet->m_body + 1);
+ break;
+ }
+ case RTMP_PACKET_TYPE_INFO:
+ // metadata (notify)
+ break;
+
+ case RTMP_PACKET_TYPE_SHARED_OBJECT:
+ /* shared object */
+ break;
+
+ case RTMP_PACKET_TYPE_INVOKE:
+ // invoke
+ ret = ServeInvoke(server, which, packet, packet->m_body);
+ break;
+
+ case RTMP_PACKET_TYPE_FLASH_VIDEO:
+ /* flv */
+ break;
+
+ default:
+ RTMP_Log(RTMP_LOGDEBUG, "%s, unknown packet type received: 0x%02x", __FUNCTION__,
+ packet->m_packetType);
+#ifdef _DEBUG
+ RTMP_LogHex(RTMP_LOGDEBUG, packet->m_body, packet->m_nBodySize);
+#endif
+ }
+ return ret;
+}
+
+int
+WriteStream(char **buf, // target pointer, maybe preallocated
+ unsigned int *plen, // length of buffer if preallocated
+ uint32_t *nTimeStamp,
+ RTMPPacket *packet)
+{
+ uint32_t prevTagSize = 0;
+ int ret = -1, len = *plen;
+
+ while (1)
+ {
+ char *packetBody = packet->m_body;
+ unsigned int nPacketLen = packet->m_nBodySize;
+
+ // skip video info/command packets
+ if (packet->m_packetType == RTMP_PACKET_TYPE_VIDEO &&
+ nPacketLen == 2 && ((*packetBody & 0xf0) == 0x50))
+ {
+ ret = 0;
+ break;
+ }
+
+ if (packet->m_packetType == RTMP_PACKET_TYPE_VIDEO && nPacketLen <= 5)
+ {
+ RTMP_Log(RTMP_LOGWARNING, "ignoring too small video packet: size: %d",
+ nPacketLen);
+ ret = 0;
+ break;
+ }
+ if (packet->m_packetType == RTMP_PACKET_TYPE_AUDIO && nPacketLen <= 1)
+ {
+ RTMP_Log(RTMP_LOGWARNING, "ignoring too small audio packet: size: %d",
+ nPacketLen);
+ ret = 0;
+ break;
+ }
+#ifdef _DEBUG
+ RTMP_Log(RTMP_LOGDEBUG, "type: %02X, size: %d, TS: %d ms", packet->m_packetType,
+ nPacketLen, packet->m_nTimeStamp);
+ if (packet->m_packetType == RTMP_PACKET_TYPE_VIDEO)
+ RTMP_Log(RTMP_LOGDEBUG, "frametype: %02X", (*packetBody & 0xf0));
+#endif
+
+ // calculate packet size and reallocate buffer if necessary
+ unsigned int size = nPacketLen
+ +
+ ((packet->m_packetType == RTMP_PACKET_TYPE_AUDIO
+ || packet->m_packetType == RTMP_PACKET_TYPE_VIDEO
+ || packet->m_packetType == RTMP_PACKET_TYPE_INFO) ? 11 : 0)
+ + (packet->m_packetType != 0x16 ? 4 : 0);
+
+ if (size + 4 > len)
+ {
+ /* The extra 4 is for the case of an FLV stream without a last
+ * prevTagSize (we need extra 4 bytes to append it). */
+ *buf = (char *) realloc(*buf, size + 4);
+ if (*buf == 0)
+ {
+ RTMP_Log(RTMP_LOGERROR, "Couldn't reallocate memory!");
+ ret = -1; // fatal error
+ break;
+ }
+ }
+ char *ptr = *buf, *pend = ptr + size+4;
+
+ /* audio (RTMP_PACKET_TYPE_AUDIO), video (RTMP_PACKET_TYPE_VIDEO)
+ * or metadata (RTMP_PACKET_TYPE_INFO) packets: construct 11 byte
+ * header then add rtmp packet's data. */
+ if (packet->m_packetType == RTMP_PACKET_TYPE_AUDIO
+ || packet->m_packetType == RTMP_PACKET_TYPE_VIDEO
+ || packet->m_packetType == RTMP_PACKET_TYPE_INFO)
+ {
+ // set data type
+ //*dataType |= (((packet->m_packetType == RTMP_PACKET_TYPE_AUDIO)<<2)|(packet->m_packetType == RTMP_PACKET_TYPE_VIDEO));
+
+ (*nTimeStamp) = packet->m_nTimeStamp;
+ prevTagSize = 11 + nPacketLen;
+
+ *ptr++ = packet->m_packetType;
+ ptr = AMF_EncodeInt24(ptr, pend, nPacketLen);
+ ptr = AMF_EncodeInt24(ptr, pend, *nTimeStamp);
+ *ptr = (char) (((*nTimeStamp) & 0xFF000000) >> 24);
+ ptr++;
+
+ // stream id
+ ptr = AMF_EncodeInt24(ptr, pend, 0);
+ }
+
+ memcpy(ptr, packetBody, nPacketLen);
+ unsigned int len = nPacketLen;
+
+ // correct tagSize and obtain timestamp if we have an FLV stream
+ if (packet->m_packetType == RTMP_PACKET_TYPE_FLASH_VIDEO)
+ {
+ unsigned int pos = 0;
+
+ while (pos + 11 < nPacketLen)
+ {
+ uint32_t dataSize = AMF_DecodeInt24(packetBody + pos + 1); // size without header (11) and without prevTagSize (4)
+ *nTimeStamp = AMF_DecodeInt24(packetBody + pos + 4);
+ *nTimeStamp |= (packetBody[pos + 7] << 24);
+
+#if 0
+ /* set data type */
+ *dataType |= (((*(packetBody+pos) == RTMP_PACKET_TYPE_AUDIO) << 2)
+ | (*(packetBody+pos) == RTMP_PACKET_TYPE_VIDEO));
+#endif
+
+ if (pos + 11 + dataSize + 4 > nPacketLen)
+ {
+ if (pos + 11 + dataSize > nPacketLen)
+ {
+ RTMP_Log(RTMP_LOGERROR,
+ "Wrong data size (%u), stream corrupted, aborting!",
+ dataSize);
+ ret = -2;
+ break;
+ }
+ RTMP_Log(RTMP_LOGWARNING, "No tagSize found, appending!");
+
+ // we have to append a last tagSize!
+ prevTagSize = dataSize + 11;
+ AMF_EncodeInt32(ptr + pos + 11 + dataSize, pend, prevTagSize);
+ size += 4;
+ len += 4;
+ }
+ else
+ {
+ prevTagSize =
+ AMF_DecodeInt32(packetBody + pos + 11 + dataSize);
+
+#ifdef _DEBUG
+ RTMP_Log(RTMP_LOGDEBUG,
+ "FLV Packet: type %02X, dataSize: %lu, tagSize: %lu, timeStamp: %lu ms",
+ (unsigned char) packetBody[pos], dataSize, prevTagSize,
+ *nTimeStamp);
+#endif
+
+ if (prevTagSize != (dataSize + 11))
+ {
+#ifdef _DEBUG
+ RTMP_Log(RTMP_LOGWARNING,
+ "Tag and data size are not consitent, writing tag size according to dataSize+11: %d",
+ dataSize + 11);
+#endif
+
+ prevTagSize = dataSize + 11;
+ AMF_EncodeInt32(ptr + pos + 11 + dataSize, pend, prevTagSize);
+ }
+ }
+
+ pos += prevTagSize + 4; //(11+dataSize+4);
+ }
+ }
+ ptr += len;
+
+ if (packet->m_packetType != RTMP_PACKET_TYPE_FLASH_VIDEO)
+ { // FLV tag packets contain their own prevTagSize
+ AMF_EncodeInt32(ptr, pend, prevTagSize);
+ //ptr += 4;
+ }
+
+ ret = size;
+ break;
+ }
+
+ if (len > *plen)
+ *plen = len;
+
+ return ret; // no more media packets
+}
+
+TFTYPE
+controlServerThread(void *unused)
+{
+ char ich;
+ while (1)
+ {
+ ich = getchar();
+ switch (ich)
+ {
+ case 'q':
+ RTMP_LogPrintf("Exiting\n");
+ stopStreaming(rtmpServer);
+ free(rtmpServer);
+ exit(0);
+ break;
+ default:
+ RTMP_LogPrintf("Unknown command \'%c\', ignoring\n", ich);
+ }
+ }
+ TFRET();
+}
+
+TFTYPE doServe(void *arg) // server sock and state (our listening sock)
+{
+ STREAMING_SERVER *server = arg;
+ RTMPPacket pc = { 0 }, ps = { 0 };
+ RTMPChunk rk = { 0 };
+ char *buf = NULL;
+ unsigned int buflen = 131072;
+ int paused = FALSE;
+ int sockfd = server->sockfd;
+
+ // timeout for http requests
+ fd_set rfds;
+ struct timeval tv;
+
+ server->state = STREAMING_IN_PROGRESS;
+
+ memset(&tv, 0, sizeof(struct timeval));
+ tv.tv_sec = 5;
+
+ FD_ZERO(&rfds);
+ FD_SET(sockfd, &rfds);
+
+ if (select(sockfd + 1, &rfds, NULL, NULL, &tv) <= 0)
+ {
+ RTMP_Log(RTMP_LOGERROR, "Request timeout/select failed, ignoring request");
+ goto quit;
+ }
+ else
+ {
+ RTMP_Init(&server->rs);
+ RTMP_Init(&server->rc);
+ server->rs.m_sb.sb_socket = sockfd;
+ if (!RTMP_Serve(&server->rs))
+ {
+ RTMP_Log(RTMP_LOGERROR, "Handshake failed");
+ goto cleanup;
+ }
+ }
+
+ buf = malloc(buflen);
+
+ /* Just process the Connect request */
+ while (RTMP_IsConnected(&server->rs) && RTMP_ReadPacket(&server->rs, &ps))
+ {
+ if (!RTMPPacket_IsReady(&ps))
+ continue;
+ ServePacket(server, 0, &ps);
+ RTMPPacket_Free(&ps);
+ if (RTMP_IsConnected(&server->rc))
+ break;
+ }
+
+ pc.m_chunk = &rk;
+
+ /* We have our own timeout in select() */
+ server->rc.Link.timeout = 10;
+ server->rs.Link.timeout = 10;
+ while (RTMP_IsConnected(&server->rs) || RTMP_IsConnected(&server->rc))
+ {
+ int n;
+ int sr, cr;
+
+ cr = server->rc.m_sb.sb_size;
+ sr = server->rs.m_sb.sb_size;
+
+ if (cr || sr)
+ {
+ }
+ else
+ {
+ n = server->rs.m_sb.sb_socket;
+ if (server->rc.m_sb.sb_socket > n)
+ n = server->rc.m_sb.sb_socket;
+ FD_ZERO(&rfds);
+ if (RTMP_IsConnected(&server->rs))
+ FD_SET(sockfd, &rfds);
+ if (RTMP_IsConnected(&server->rc))
+ FD_SET(server->rc.m_sb.sb_socket, &rfds);
+
+ /* give more time to start up if we're not playing yet */
+ tv.tv_sec = server->f_cur ? 30 : 60;
+ tv.tv_usec = 0;
+
+ if (select(n + 1, &rfds, NULL, NULL, &tv) <= 0)
+ {
+ if (server->f_cur && server->rc.m_mediaChannel && !paused)
+ {
+ server->rc.m_pauseStamp = server->rc.m_channelTimestamp[server->rc.m_mediaChannel];
+ if (RTMP_ToggleStream(&server->rc))
+ {
+ paused = TRUE;
+ continue;
+ }
+ }
+ RTMP_Log(RTMP_LOGERROR, "Request timeout/select failed, ignoring request");
+ goto cleanup;
+ }
+ if (server->rs.m_sb.sb_socket > 0 &&
+ FD_ISSET(server->rs.m_sb.sb_socket, &rfds))
+ sr = 1;
+ if (server->rc.m_sb.sb_socket > 0 &&
+ FD_ISSET(server->rc.m_sb.sb_socket, &rfds))
+ cr = 1;
+ }
+ if (sr)
+ {
+ while (RTMP_ReadPacket(&server->rs, &ps))
+ if (RTMPPacket_IsReady(&ps))
+ {
+ /* change chunk size */
+ if (ps.m_packetType == RTMP_PACKET_TYPE_CHUNK_SIZE)
+ {
+ if (ps.m_nBodySize >= 4)
+ {
+ server->rs.m_inChunkSize = AMF_DecodeInt32(ps.m_body);
+ RTMP_Log(RTMP_LOGDEBUG, "%s, client: chunk size change to %d", __FUNCTION__,
+ server->rs.m_inChunkSize);
+ server->rc.m_outChunkSize = server->rs.m_inChunkSize;
+ }
+ }
+ /* bytes received */
+ else if (ps.m_packetType == RTMP_PACKET_TYPE_BYTES_READ_REPORT)
+ {
+ if (ps.m_nBodySize >= 4)
+ {
+ int count = AMF_DecodeInt32(ps.m_body);
+ RTMP_Log(RTMP_LOGDEBUG, "%s, client: bytes received = %d", __FUNCTION__,
+ count);
+ }
+ }
+ /* ctrl */
+ else if (ps.m_packetType == RTMP_PACKET_TYPE_CONTROL)
+ {
+ short nType = AMF_DecodeInt16(ps.m_body);
+ /* UpdateBufferMS */
+ if (nType == 0x03)
+ {
+ char *ptr = ps.m_body+2;
+ int id;
+ int len;
+ id = AMF_DecodeInt32(ptr);
+ /* Assume the interesting media is on a non-zero stream */
+ if (id)
+ {
+ len = AMF_DecodeInt32(ptr+4);
+#if 1
+ /* request a big buffer */
+ if (len < BUFFERTIME)
+ {
+ AMF_EncodeInt32(ptr+4, ptr+8, BUFFERTIME);
+ }
+#endif
+ RTMP_Log(RTMP_LOGDEBUG, "%s, client: BufferTime change in stream %d to %d", __FUNCTION__,
+ id, len);
+ }
+ }
+ }
+ else if (ps.m_packetType == RTMP_PACKET_TYPE_FLEX_MESSAGE
+ || ps.m_packetType == RTMP_PACKET_TYPE_INVOKE)
+ {
+ if (ServePacket(server, 0, &ps) && server->f_cur)
+ {
+ fclose(server->f_cur->f_file);
+ server->f_cur->f_file = NULL;
+ server->f_cur = NULL;
+ }
+ }
+ RTMP_SendPacket(&server->rc, &ps, FALSE);
+ RTMPPacket_Free(&ps);
+ break;
+ }
+ }
+ if (cr)
+ {
+ while (RTMP_ReadPacket(&server->rc, &pc))
+ {
+ int sendit = 1;
+ if (RTMPPacket_IsReady(&pc))
+ {
+ if (paused)
+ {
+ if (pc.m_nTimeStamp <= server->rc.m_mediaStamp)
+ continue;
+ paused = 0;
+ server->rc.m_pausing = 0;
+ }
+ /* change chunk size */
+ if (pc.m_packetType == RTMP_PACKET_TYPE_CHUNK_SIZE)
+ {
+ if (pc.m_nBodySize >= 4)
+ {
+ server->rc.m_inChunkSize = AMF_DecodeInt32(pc.m_body);
+ RTMP_Log(RTMP_LOGDEBUG, "%s, server: chunk size change to %d", __FUNCTION__,
+ server->rc.m_inChunkSize);
+ server->rs.m_outChunkSize = server->rc.m_inChunkSize;
+ }
+ }
+ else if (pc.m_packetType == RTMP_PACKET_TYPE_CONTROL)
+ {
+ short nType = AMF_DecodeInt16(pc.m_body);
+ /* SWFverification */
+ if (nType == 0x1a)
+#ifdef CRYPTO
+ if (server->rc.Link.SWFSize)
+ {
+ RTMP_SendCtrl(&server->rc, 0x1b, 0, 0);
+ sendit = 0;
+ }
+#else
+ /* The session will certainly fail right after this */
+ RTMP_Log(RTMP_LOGERROR, "%s, server requested SWF verification, need CRYPTO support! ", __FUNCTION__);
+#endif
+ }
+ else if (server->f_cur && (
+ pc.m_packetType == RTMP_PACKET_TYPE_AUDIO ||
+ pc.m_packetType == RTMP_PACKET_TYPE_VIDEO ||
+ pc.m_packetType == RTMP_PACKET_TYPE_INFO ||
+ pc.m_packetType == RTMP_PACKET_TYPE_FLASH_VIDEO) &&
+ RTMP_ClientPacket(&server->rc, &pc))
+ {
+ int len = WriteStream(&buf, &buflen, &server->stamp, &pc);
+ if (len > 0 && fwrite(buf, 1, len, server->f_cur->f_file) != len)
+ goto cleanup;
+ }
+ else if (pc.m_packetType == RTMP_PACKET_TYPE_FLEX_MESSAGE ||
+ pc.m_packetType == RTMP_PACKET_TYPE_INVOKE)
+ {
+ if (ServePacket(server, 1, &pc) && server->f_cur)
+ {
+ fclose(server->f_cur->f_file);
+ server->f_cur->f_file = NULL;
+ server->f_cur = NULL;
+ }
+ }
+ }
+ if (sendit && RTMP_IsConnected(&server->rs))
+ RTMP_SendChunk(&server->rs, &rk);
+ if (RTMPPacket_IsReady(&pc))
+ RTMPPacket_Free(&pc);
+ break;
+ }
+ }
+ if (!RTMP_IsConnected(&server->rs) && RTMP_IsConnected(&server->rc)
+ && !server->f_cur)
+ RTMP_Close(&server->rc);
+ }
+
+cleanup:
+ RTMP_LogPrintf("Closing connection... ");
+ RTMP_Close(&server->rs);
+ RTMP_Close(&server->rc);
+ while (server->f_head)
+ {
+ Flist *fl = server->f_head;
+ server->f_head = fl->f_next;
+ if (fl->f_file)
+ fclose(fl->f_file);
+ free(fl);
+ }
+ server->f_tail = NULL;
+ server->f_cur = NULL;
+ free(buf);
+ /* Should probably be done by RTMP_Close() ... */
+ server->rc.Link.hostname.av_val = NULL;
+ server->rc.Link.tcUrl.av_val = NULL;
+ server->rc.Link.swfUrl.av_val = NULL;
+ server->rc.Link.pageUrl.av_val = NULL;
+ server->rc.Link.app.av_val = NULL;
+ server->rc.Link.auth.av_val = NULL;
+ server->rc.Link.flashVer.av_val = NULL;
+ RTMP_LogPrintf("done!\n\n");
+
+quit:
+ if (server->state == STREAMING_IN_PROGRESS)
+ server->state = STREAMING_ACCEPTING;
+
+ TFRET();
+}
+
+TFTYPE
+serverThread(void *arg)
+{
+ STREAMING_SERVER *server = arg;
+ server->state = STREAMING_ACCEPTING;
+
+ while (server->state == STREAMING_ACCEPTING)
+ {
+ struct sockaddr_in addr;
+ socklen_t addrlen = sizeof(struct sockaddr_in);
+ STREAMING_SERVER *srv2 = malloc(sizeof(STREAMING_SERVER));
+ int sockfd =
+ accept(server->sockfd, (struct sockaddr *) &addr, &addrlen);
+
+ if (sockfd > 0)
+ {
+#ifdef linux
+ struct sockaddr_in dest;
+ char destch[16];
+ socklen_t destlen = sizeof(struct sockaddr_in);
+ getsockopt(sockfd, SOL_IP, SO_ORIGINAL_DST, &dest, &destlen);
+ strcpy(destch, inet_ntoa(dest.sin_addr));
+ RTMP_Log(RTMP_LOGDEBUG, "%s: accepted connection from %s to %s\n", __FUNCTION__,
+ inet_ntoa(addr.sin_addr), destch);
+#else
+ RTMP_Log(RTMP_LOGDEBUG, "%s: accepted connection from %s\n", __FUNCTION__,
+ inet_ntoa(addr.sin_addr));
+#endif
+ *srv2 = *server;
+ srv2->sockfd = sockfd;
+ /* Create a new thread and transfer the control to that */
+ ThreadCreate(doServe, srv2);
+ RTMP_Log(RTMP_LOGDEBUG, "%s: processed request\n", __FUNCTION__);
+ }
+ else
+ {
+ RTMP_Log(RTMP_LOGERROR, "%s: accept failed", __FUNCTION__);
+ }
+ }
+ server->state = STREAMING_STOPPED;
+ TFRET();
+}
+
+STREAMING_SERVER *
+startStreaming(const char *address, int port)
+{
+ struct sockaddr_in addr;
+ int sockfd, tmp;
+ STREAMING_SERVER *server;
+
+ sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
+ if (sockfd == -1)
+ {
+ RTMP_Log(RTMP_LOGERROR, "%s, couldn't create sock", __FUNCTION__);
+ return 0;
+ }
+
+ tmp = 1;
+ setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR,
+ (char *) &tmp, sizeof(tmp) );
+
+ addr.sin_family = AF_INET;
+ addr.sin_addr.s_addr = inet_addr(address); //htonl(INADDR_ANY);
+ addr.sin_port = htons(port);
+
+ if (bind(sockfd, (struct sockaddr *) &addr, sizeof(struct sockaddr_in)) ==
+ -1)
+ {
+ RTMP_Log(RTMP_LOGERROR, "%s, TCP bind failed for port number: %d", __FUNCTION__,
+ port);
+ return 0;
+ }
+
+ if (listen(sockfd, 10) == -1)
+ {
+ RTMP_Log(RTMP_LOGERROR, "%s, listen failed", __FUNCTION__);
+ closesocket(sockfd);
+ return 0;
+ }
+
+ server = (STREAMING_SERVER *) calloc(1, sizeof(STREAMING_SERVER));
+ server->sockfd = sockfd;
+
+ ThreadCreate(serverThread, server);
+
+ return server;
+}
+
+void
+stopStreaming(STREAMING_SERVER * server)
+{
+ assert(server);
+
+ if (server->state != STREAMING_STOPPED)
+ {
+ int fd = server->sockfd;
+ server->sockfd = 0;
+ if (server->state == STREAMING_IN_PROGRESS)
+ {
+ server->state = STREAMING_STOPPING;
+
+ // wait for streaming threads to exit
+ while (server->state != STREAMING_STOPPED)
+ msleep(1);
+ }
+
+ if (fd && closesocket(fd))
+ RTMP_Log(RTMP_LOGERROR, "%s: Failed to close listening sock, error %d",
+ __FUNCTION__, GetSockError());
+
+ server->state = STREAMING_STOPPED;
+ }
+}
+
+
+void
+sigIntHandler(int sig)
+{
+ RTMP_ctrlC = TRUE;
+ RTMP_LogPrintf("Caught signal: %d, cleaning up, just a second...\n", sig);
+ if (rtmpServer)
+ stopStreaming(rtmpServer);
+ signal(SIGINT, SIG_DFL);
+}
+
+int
+RtmpSuckMain(int logAll)
+{
+ int nStatus = RD_SUCCESS;
+
+ // rtmp streaming server
+ char DEFAULT_RTMP_STREAMING_DEVICE[] = "0.0.0.0"; // 0.0.0.0 is any device
+
+ char *rtmpStreamingDevice = DEFAULT_RTMP_STREAMING_DEVICE; // streaming device, default 0.0.0.0
+ int nRtmpStreamingPort = 1935; // port
+
+ RTMP_LogPrintf("RTMP Proxy Server %s\n", RTMPDUMP_VERSION);
+ RTMP_LogPrintf("(c) 2010 Andrej Stepanchuk, Howard Chu; license: GPL\n\n");
+
+ RTMP_debuglevel = RTMP_LOGINFO;
+
+ if (logAll)
+ RTMP_debuglevel = RTMP_LOGALL;
+
+ signal(SIGINT, sigIntHandler);
+#ifndef WIN32
+ signal(SIGPIPE, SIG_IGN);
+#endif
+
+#ifdef _DEBUG
+ netstackdump = fopen("netstackdump", "wb");
+ netstackdump_read = fopen("netstackdump_read", "wb");
+#endif
+
+ InitSockets();
+
+ // start text UI
+ // ThreadCreate(controlServerThread, 0);
+
+ // start http streaming
+ if ((rtmpServer =
+ startStreaming(rtmpStreamingDevice, nRtmpStreamingPort)) == 0)
+ {
+ RTMP_Log(RTMP_LOGERROR, "Failed to start RTMP server, exiting!");
+ return RD_FAILED;
+ }
+ RTMP_LogPrintf("Streaming on rtmp://%s:%d\n", rtmpStreamingDevice,
+ nRtmpStreamingPort);
+
+ while (rtmpServer->state != STREAMING_STOPPED)
+ {
+ sleep(1);
+ }
+ RTMP_Log(RTMP_LOGDEBUG, "Done, exiting...");
+
+ free(rtmpServer);
+
+ CleanupSockets();
+
+#ifdef _DEBUG
+ if (netstackdump != 0)
+ fclose(netstackdump);
+ if (netstackdump_read != 0)
+ fclose(netstackdump_read);
+#endif
+ return nStatus;
+}
diff --git a/app/src/main/cpp/rtmp/rtmpsuck.h b/app/src/main/cpp/rtmp/rtmpsuck.h
new file mode 100644
index 00000000..b89d2e7c
--- /dev/null
+++ b/app/src/main/cpp/rtmp/rtmpsuck.h
@@ -0,0 +1,51 @@
+//
+// Created by Matthew on 2025/1/27.
+//
+
+#ifndef MICROPHOTO_RTMPSUCK_H
+#define MICROPHOTO_RTMPSUCK_H
+
+#include
+#include
+
+enum
+{
+ STREAMING_ACCEPTING,
+ STREAMING_IN_PROGRESS,
+ STREAMING_STOPPING,
+ STREAMING_STOPPED
+};
+
+typedef struct Flist
+{
+ struct Flist *f_next;
+ FILE *f_file;
+ AVal f_path;
+} Flist;
+
+typedef struct Plist
+{
+ struct Plist *p_next;
+ RTMPPacket p_pkt;
+} Plist;
+
+typedef struct
+{
+ int sockfd;
+ int state;
+ uint32_t stamp;
+ RTMP rs;
+ RTMP rc;
+ Plist *rs_pkt[2]; /* head, tail */
+ Plist *rc_pkt[2]; /* head, tail */
+ Flist *f_head, *f_tail;
+ Flist *f_cur;
+
+} STREAMING_SERVER;
+
+STREAMING_SERVER *startStreaming(const char *address, int port);
+void stopStreaming(STREAMING_SERVER * server);
+
+int RtmpSuckMain(int logAll);
+
+#endif //MICROPHOTO_RTMPSUCK_H
diff --git a/app/src/main/cpp/rtmp/thread.c b/app/src/main/cpp/rtmp/thread.c
new file mode 100644
index 00000000..3f367983
--- /dev/null
+++ b/app/src/main/cpp/rtmp/thread.c
@@ -0,0 +1,58 @@
+/* Thread compatibility glue
+ * Copyright (C) 2009 Howard Chu
+ *
+ * This Program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * This Program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with RTMPDump; see the file COPYING. If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ */
+
+#include "thread.h"
+#include
+
+#ifdef WIN32
+
+#include
+
+HANDLE
+ThreadCreate(thrfunc *routine, void *args)
+{
+ HANDLE thd;
+
+ thd = (HANDLE) _beginthread(routine, 0, args);
+ if (thd == -1L)
+ RTMP_LogPrintf("%s, _beginthread failed with %d\n", __FUNCTION__, errno);
+
+ return thd;
+}
+#else
+pthread_t
+ThreadCreate(thrfunc *routine, void *args)
+{
+ pthread_t id = 0;
+ pthread_attr_t attributes;
+ int ret;
+
+ pthread_attr_init(&attributes);
+ pthread_attr_setdetachstate(&attributes, PTHREAD_CREATE_DETACHED);
+
+ ret =
+ pthread_create(&id, &attributes, routine, args);
+ if (ret != 0)
+ RTMP_LogPrintf("%s, pthread_create failed with %d\n", __FUNCTION__, ret);
+
+ return id;
+}
+#endif
diff --git a/app/src/main/cpp/rtmp/thread.h b/app/src/main/cpp/rtmp/thread.h
new file mode 100644
index 00000000..e8deae65
--- /dev/null
+++ b/app/src/main/cpp/rtmp/thread.h
@@ -0,0 +1,40 @@
+/* Thread compatibility glue
+ * Copyright (C) 2009 Howard Chu
+ *
+ * This Program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2, or (at your option)
+ * any later version.
+ *
+ * This Program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with RTMPDump; see the file COPYING. If not, write to
+ * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ */
+
+#ifndef __THREAD_H__
+#define __THREAD_H__ 1
+
+#ifdef WIN32
+#include
+#include
+#define TFTYPE void
+#define TFRET()
+#define THANDLE HANDLE
+#else
+#include
+#define TFTYPE void *
+#define TFRET() return 0
+#define THANDLE pthread_t
+#endif
+typedef TFTYPE (thrfunc)(void *arg);
+
+THANDLE ThreadCreate(thrfunc *routine, void *args);
+#endif /* __THREAD_H__ */
diff --git a/app/src/main/java/com/xypower/mpapp/MainActivity.java b/app/src/main/java/com/xypower/mpapp/MainActivity.java
index 99ea14fc..12e3aa03 100644
--- a/app/src/main/java/com/xypower/mpapp/MainActivity.java
+++ b/app/src/main/java/com/xypower/mpapp/MainActivity.java
@@ -7,16 +7,11 @@ import android.content.Intent;
import android.content.pm.PackageManager;
import android.location.Location;
import android.location.LocationListener;
-import android.location.LocationManager;
import android.os.Build;
-import android.os.FileObserver;
import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
import android.os.Messenger;
import android.os.StrictMode;
-import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.core.app.ActivityCompat;
@@ -24,9 +19,7 @@ import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.telephony.SubscriptionManager;
-import android.telephony.TelephonyManager;
import android.text.TextUtils;
-import android.text.method.ScrollingMovementMethod;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
@@ -38,7 +31,6 @@ import com.xypower.common.CameraUtils;
import com.xypower.common.MicroPhotoContext;
import com.xypower.mpapp.databinding.ActivityMainBinding;
import com.xypower.mpapp.utils.LocationUtil;
-import com.xypower.mpapp.utils.RandomReader;
import java.io.File;
@@ -427,6 +419,28 @@ public class MainActivity extends AppCompatActivity {
}
});
+ this.binding.btnStartRtmp.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ Context appContext = getApplicationContext();
+
+ startRtmpSuckService(appContext);
+
+ binding.btnStartRtmp.setEnabled(false);
+ binding.btnStopRtmp.setEnabled(true);
+ }
+ });
+
+ this.binding.btnStopRtmp.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+
+ RtmpService.stopRtmpService(getApplicationContext());
+
+ binding.btnStartRtmp.setEnabled(true);
+ binding.btnStopRtmp.setEnabled(false);
+ }
+ });
}
public static void startMicroPhotoService(Context context, MicroPhotoContext.AppConfig curAppConfig, Messenger messenger) {
@@ -454,6 +468,19 @@ public class MainActivity extends AppCompatActivity {
}
}
+ public static void startRtmpSuckService(Context context) {
+
+ Intent intent = new Intent(context, RtmpService.class);
+ intent.setAction(RtmpService.ACTION_START);
+
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ context.startForegroundService(intent);
+ } else {
+ context.startService(intent);
+ }
+ }
+
private void takePhoto(int channel, int preset, boolean photoOrVideo) {
if (binding.btnStartServ.isEnabled()) {
String appPath = MicroPhotoContext.buildMpAppDir(getApplicationContext());
diff --git a/app/src/main/java/com/xypower/mpapp/RtmpService.java b/app/src/main/java/com/xypower/mpapp/RtmpService.java
new file mode 100644
index 00000000..7f60081d
--- /dev/null
+++ b/app/src/main/java/com/xypower/mpapp/RtmpService.java
@@ -0,0 +1,230 @@
+package com.xypower.mpapp;
+
+import android.app.AlarmManager;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.SystemClock;
+import android.util.Log;
+import android.widget.RemoteViews;
+import android.widget.Toast;
+
+import androidx.core.app.NotificationCompat;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class RtmpService extends Service {
+ public RtmpService() {
+ }
+
+
+ static {
+ System.loadLibrary("rtmpdump");
+ }
+
+ public static final String TAG = "RTMP";
+
+ public static final String ACTION_START = "com.xypower.mprtmp.ACT_START";
+ public static final String ACTION_STOP = "com.xypower.mprtmp.ACT_STOP";
+ public static final String ACTION_MAIN = "com.xypower.mprtmp.ACT_MAIN";
+
+ public static final int NOTIFICATION_ID_FOREGROUND_SERVICE = 8466603;
+
+ private static final String FOREGROUND_CHANNEL_ID = "fg_rtmp";
+
+ public static class STATE_SERVICE {
+ public static final int CONNECTED = 10;
+ public static final int NOT_CONNECTED = 0;
+ }
+
+ private static int mStateService = STATE_SERVICE.NOT_CONNECTED;
+
+ private NotificationManager mNotificationManager;
+ private ScreenActionReceiver mScreenaActionReceiver = null;
+
+ private Handler mHander = null;
+ private Thread mServiceThread;
+ private long mNativeHandle = 0;
+
+ static AtomicInteger reqCode = new AtomicInteger(100);
+
+ private native long startService();
+ private native void stopService(long nativeHandle);
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ // TODO: Return the communication channel to the service.
+ throw new UnsupportedOperationException("Not yet implemented");
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ mHander = new Handler();
+
+ mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+ mStateService = STATE_SERVICE.NOT_CONNECTED;
+
+ mScreenaActionReceiver = new ScreenActionReceiver();
+ }
+
+ @Override
+ public void onDestroy() {
+
+ mStateService = STATE_SERVICE.NOT_CONNECTED;
+
+ unregisterReceiver(mScreenaActionReceiver);
+
+ super.onDestroy();
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+
+ if (intent == null) {
+ stopForeground(true);
+ stopSelf();
+ return START_NOT_STICKY;
+ }
+
+ // if user starts the service
+ switch (intent.getAction()) {
+ case ACTION_START:
+ Log.d(TAG, "Received user starts foreground intent");
+ startForeground(NOTIFICATION_ID_FOREGROUND_SERVICE, prepareNotification());
+
+ connect();
+
+ registerReceiver(mScreenaActionReceiver, mScreenaActionReceiver.getFilter());
+
+ if (mServiceThread == null) {
+
+ mServiceThread = new Thread(new Runnable() {
+ @Override
+ public void run() {
+
+ mNativeHandle = startService();
+
+ Log.d(TAG, "RTMP service finishes");
+ }
+ });
+
+ mServiceThread.start();
+ }
+
+ break;
+ case ACTION_STOP:
+ unregisterReceiver(mScreenaActionReceiver);
+
+ stopForeground(true);
+ stopSelf();
+ break;
+ default:
+ stopForeground(true);
+ stopSelf();
+ }
+
+ return START_NOT_STICKY;
+ }
+
+ private void connect() {
+ // after 10 seconds its connected
+ mHander.postDelayed(
+ new Runnable() {
+ public void run() {
+ // Log.d(TAG, "Bluetooth Low Energy device is connected!!");
+ Toast.makeText(getApplicationContext(), "RTMP Connected!", Toast.LENGTH_SHORT).show();
+ mStateService = STATE_SERVICE.CONNECTED;
+ startForeground(NOTIFICATION_ID_FOREGROUND_SERVICE, prepareNotification());
+ }
+ }, 10000);
+
+ }
+
+ private Notification prepareNotification() {
+ // handle build version above android oreo
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O &&
+ mNotificationManager.getNotificationChannel(FOREGROUND_CHANNEL_ID) == null) {
+ CharSequence name = getString(R.string.text_name_notification);
+ int importance = NotificationManager.IMPORTANCE_DEFAULT;
+ NotificationChannel channel = new NotificationChannel(FOREGROUND_CHANNEL_ID, name, importance);
+ channel.enableVibration(false);
+ mNotificationManager.createNotificationChannel(channel);
+ }
+
+ Intent notificationIntent = new Intent(this, MainActivity.class);
+ notificationIntent.setAction(ACTION_MAIN);
+ notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+
+ // if min sdk goes below honeycomb
+ /*if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.HONEYCOMB) {
+ notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ } else {
+ notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ }*/
+
+ int uniqueReqCode = reqCode.getAndIncrement();
+ PendingIntent pendingIntent = PendingIntent.getActivity(this, uniqueReqCode, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+
+ // make a stop intent
+ Intent stopIntent = new Intent(this, RtmpService.class);
+ stopIntent.setAction(ACTION_STOP);
+ uniqueReqCode = reqCode.getAndIncrement();
+ PendingIntent pendingStopIntent = PendingIntent.getService(this, uniqueReqCode, stopIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+ RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.notification);
+ remoteViews.setOnClickPendingIntent(R.id.btn_stop, pendingStopIntent);
+
+ // if it is connected
+ switch (mStateService) {
+ case STATE_SERVICE.NOT_CONNECTED:
+ remoteViews.setTextViewText(R.id.tv_state, "DISCONNECTED");
+ break;
+ case STATE_SERVICE.CONNECTED:
+ remoteViews.setTextViewText(R.id.tv_state, "CONNECTED");
+ break;
+ }
+
+ // notification builder
+ NotificationCompat.Builder notificationBuilder;
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+ notificationBuilder = new NotificationCompat.Builder(this, FOREGROUND_CHANNEL_ID);
+ } else {
+ notificationBuilder = new NotificationCompat.Builder(this);
+ }
+ notificationBuilder
+ .setContent(remoteViews)
+ .setSmallIcon(R.drawable.ic_rtmpsuck)
+ .setCategory(NotificationCompat.CATEGORY_SERVICE)
+ .setOnlyAlertOnce(true)
+ .setOngoing(true)
+ .setAutoCancel(true)
+ .setContentIntent(pendingIntent);
+
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
+ notificationBuilder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
+ }
+
+ return notificationBuilder.build();
+ }
+
+ public static void stopRtmpService(Context context) {
+
+ Intent alarmIntent = new Intent();
+
+ alarmIntent.setPackage(context.getPackageName());
+ alarmIntent.setAction(ACTION_STOP);
+ int uniqueReqCode = reqCode.getAndIncrement();
+ PendingIntent pendingIntent = PendingIntent.getBroadcast(context.getApplicationContext(), uniqueReqCode, alarmIntent, 0);
+
+ AlarmManager alarmManager = (AlarmManager) context.getApplicationContext().getSystemService(ALARM_SERVICE);
+ alarmManager.setExactAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + 100, pendingIntent);
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_rtmpsuck.xml b/app/src/main/res/drawable/ic_rtmpsuck.xml
new file mode 100644
index 00000000..cfa3ae51
--- /dev/null
+++ b/app/src/main/res/drawable/ic_rtmpsuck.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/layout-land/activity_main.xml b/app/src/main/res/layout-land/activity_main.xml
index 36b42e29..b74c9b13 100644
--- a/app/src/main/res/layout-land/activity_main.xml
+++ b/app/src/main/res/layout-land/activity_main.xml
@@ -260,6 +260,17 @@
app:layout_constraintStart_toEndOf="@+id/btnTakePhoto3"
app:layout_constraintTop_toTopOf="@+id/btnTakePhoto" />
+
+
+
+
+
+
+
+