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" /> +