├── README.txt ├── example.py ├── module.cpp └── setup.py /README.txt: -------------------------------------------------------------------------------- 1 | Mike McCandless, mikemccand at gmail.com 2 | 3 | This contains a small Python wrapper around the Live555 Streaming 4 | Media APIs, so that you can load video frames. It only wraps a tiny, 5 | tiny subset of all of Live555's APIs, specifically the APIs necessary 6 | to pull frames via RTSP/RTP from an IP camera. 7 | 8 | I've only tested on Linux with Python 3, with the surprisingly 9 | excellent Lorex LNB2151/LNB2153 cameras, with H264 video. Please 10 | report back if you succeed with other cameras. 11 | 12 | INSTRUCTIONS: 13 | 14 | * First, download and compile/install the Live555 library from 15 | http://www.live555.com/liveMedia/public, and unzip/tar it and run: 16 | 17 | * ./genMakefiles linux 18 | * export CPPFLAGS=-fPIC CFLAGS=-fPIC 19 | * make 20 | * [optional: make install] 21 | 22 | * If you unzip/tar'd Live555 in this directory (the pylive555 23 | checkout), to the sub-directory "live", then you can skip this 24 | step; otherwise, edit INSTALL_DIR in setup.py to point the live 25 | headers and libraries. 26 | 27 | * Build the python bindings: python3 setup.py build; make sure 28 | there are no errors. 29 | 30 | * Copy the resulting .so (from build/lib/*.so) to somewhere onto 31 | your PYTHONPATH. 32 | 33 | * Run the example 34 | 35 | python3 example.py 10.17.4.118 1 10 out.264 36 | 37 | That will record 10 seconds of H264 video from the camera at 38 | 10.17.4.118, channel 1, saving it to the file out.264. 39 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | import time 2 | import sys 3 | import live555 4 | import threading 5 | 6 | # Shows how to use live555 module to pull frames from an RTSP/RTP 7 | # source. Run this (likely first customizing the URL below: 8 | 9 | # Example: python3 example.py 10.17.4.118 admin 10 out.264 10 | if len(sys.argv) != 5: 11 | print() 12 | print('Usage: python3 example.py cameraIP username seconds fileOut') 13 | print() 14 | sys.exit(1) 15 | 16 | cameraIP = sys.argv[1] 17 | username = sys.argv[2] 18 | seconds = float(sys.argv[3]) 19 | fileOut = sys.argv[4] 20 | 21 | password = input("Enter password:") 22 | 23 | # NOTE: the username & password, and the URL path, will vary from one 24 | # camera to another! This URL path works with HikVision/Dahua NVR and pulls a set of DVR channels: 25 | url = 'rtsp://{username}:{password}@{cameraIP}/Streaming/Channels/{channel}' 26 | 27 | fOut = open(fileOut, 'wb') 28 | 29 | def shutdownCallback(): 30 | # Note this should never actually be called because callbacks upon shutdown are disabled due to Python issue 23571: 31 | # https://bugs.python.org/issue23571 32 | print('shutdown callback') 33 | 34 | def oneFrame(codecName, bytes, sec, usec, durUSec): 35 | print('frame for %s: %d bytes' % (codecName, len(bytes))) 36 | fOut.write(b'\0\0\0\1' + bytes) 37 | 38 | def oneFrame2(codecName, bytes, sec, usec, durUSec): 39 | print('frame (handle2) for %s: %d bytes' % (codecName, len(bytes))) 40 | fOut.write(b'\0\0\0\1' + bytes) 41 | 42 | # Starts pulling frames from the URL, with the provided callback: 43 | useTCP = True 44 | handle = live555.startRTSP(url.format(username=username, password=password, cameraIP=cameraIP, channel=102), oneFrame, shutdownCallback, useTCP) 45 | print('got handle {}'.format(handle)) 46 | 47 | handle2 = live555.startRTSP(url.format(username=username, password=password, cameraIP=cameraIP, channel=202), oneFrame2, shutdownCallback, useTCP) 48 | print('got handle2 {}'.format(handle2)) 49 | 50 | # Run Live555's event loop in a background thread: 51 | t = threading.Thread(target=live555.runEventLoop, args=()) 52 | t.setDaemon(True) 53 | t.start() 54 | 55 | endTime = time.time() + seconds 56 | while time.time() < endTime: 57 | time.sleep(0.1) 58 | 59 | def oneFrame3(codecName, bytes, sec, usec, durUSec): 60 | print('frame (handle3) for %s: %d bytes' % (codecName, len(bytes))) 61 | fOut.write(b'\0\0\0\1' + bytes) 62 | 63 | handle3 = live555.startRTSP(url.format(username=username, password=password, cameraIP=cameraIP, channel=302), oneFrame3, shutdownCallback, useTCP) 64 | print('got handle3: {}'.format(handle3)) 65 | 66 | print('stopping first handle') 67 | try: 68 | live555.stopRTSP(handle) 69 | except live555.error as e: 70 | print('live555 error received') 71 | print('stopped first handle') 72 | endTime = time.time() + seconds 73 | while time.time() < endTime: 74 | time.sleep(0.1) 75 | 76 | print('stopping 2nd handle') 77 | try: 78 | live555.stopRTSP(handle2) 79 | except live555.error as e: 80 | print('live555 error received') 81 | pass 82 | print('stopped 2nd handle') 83 | handle = live555.startRTSP(url.format(username=username, password=password, cameraIP=cameraIP, channel=102), oneFrame, shutdownCallback, useTCP) 84 | print('got handle {}'.format(handle)) 85 | 86 | endTime = time.time() + seconds 87 | while time.time() < endTime: 88 | time.sleep(0.1) 89 | 90 | print('stopping 3rd handle') 91 | try: 92 | live555.stopRTSP(handle3) 93 | except live555.error as e: 94 | print('live555 error received') 95 | pass 96 | print('stopped 3rd handle') 97 | time.sleep(5) 98 | print('slept after 3rd handle') 99 | 100 | try: 101 | live555.stopRTSP(handle) 102 | except live555.error as e: 103 | print('live555 error received') 104 | pass 105 | 106 | # Tell Live555's event loop to stop: 107 | try: 108 | live555.stopEventLoop() 109 | except: 110 | import traceback 111 | print('exception received') 112 | traceback.print_exc() 113 | 114 | # Wait for the background thread to finish: 115 | t.join() 116 | 117 | -------------------------------------------------------------------------------- /module.cpp: -------------------------------------------------------------------------------- 1 | // License from live555's testRTSPClient: 2 | 3 | /********** 4 | This library is free software; you can redistribute it and/or modify it under 5 | the terms of the GNU Lesser General Public License as published by the 6 | Free Software Foundation; either version 2.1 of the License, or (at your 7 | option) any later version. (See .) 8 | 9 | This library is distributed in the hope that it will be useful, but WITHOUT 10 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 11 | FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for 12 | more details. 13 | 14 | You should have received a copy of the GNU Lesser General Public License 15 | along with this library; if not, write to the Free Software Foundation, Inc., 16 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 17 | **********/ 18 | 19 | /* Basic Python wrapper around live555's APIs for loading RTSP 20 | * streams */ 21 | 22 | #include 23 | 24 | #include "liveMedia.hh" 25 | #include "BasicUsageEnvironment.hh" 26 | 27 | #define MAX_CLIENTS 1000 28 | 29 | // Forward function definitions: 30 | 31 | // RTSP 'response handlers': 32 | void continueAfterDESCRIBE(RTSPClient* rtspClient, int resultCode, char* resultString); 33 | void continueAfterSETUP(RTSPClient* rtspClient, int resultCode, char* resultString); 34 | void continueAfterPLAY(RTSPClient* rtspClient, int resultCode, char* resultString); 35 | 36 | // Other event handler functions: 37 | void subsessionAfterPlaying(void* clientData); // called when a stream's subsession (e.g., audio or video substream) ends 38 | void subsessionByeHandler(void* clientData); // called when a RTCP "BYE" is received for a subsession 39 | void streamTimerHandler(void* clientData); 40 | // called at the end of a stream's expected duration (if the stream has not already signaled its end using a RTCP "BYE") 41 | 42 | // Used to iterate through each stream's 'subsessions', setting up each one: 43 | void setupNextSubsession(RTSPClient* rtspClient); 44 | 45 | // Used to shut down and close a stream (including its "RTSPClient" object): 46 | void shutdownStream(RTSPClient* rtspClient, int exitCode = 1); 47 | 48 | #define RTSP_CLIENT_VERBOSITY_LEVEL 0 // by default, print verbose output from each "RTSPClient" 49 | 50 | // If you don't want to see debugging output for each received frame, then comment out the following line: 51 | //#define DEBUG_PRINT_EACH_RECEIVED_FRAME 1 52 | 53 | static PyObject *error; 54 | 55 | static TaskScheduler* scheduler; 56 | static UsageEnvironment* env; 57 | 58 | static PyThreadState *threadState; 59 | 60 | // Define a class to hold per-stream state that we maintain throughout each stream's lifetime: 61 | class StreamClientState { 62 | public: 63 | StreamClientState(); 64 | virtual ~StreamClientState(); 65 | 66 | public: 67 | Boolean useTCP; 68 | MediaSubsessionIterator* iter; 69 | MediaSession* session; 70 | MediaSubsession* subsession; 71 | PyObject *frameCallback; 72 | PyObject* shutdownCallback; 73 | TaskToken streamTimerTask; 74 | double duration; 75 | int m_handle; 76 | }; 77 | 78 | static RTSPClient* clientList[MAX_CLIENTS]; 79 | int last_handle = -1; 80 | 81 | // If you're streaming just a single stream (i.e., just from a single URL, once), then you can define and use just a single 82 | // "StreamClientState" structure, as a global variable in your application. However, because - in this demo application - we're 83 | // showing how to play multiple streams, concurrently, we can't do that. Instead, we have to have a separate "StreamClientState" 84 | // structure for each "RTSPClient". To do this, we subclass "RTSPClient", and add a "StreamClientState" field to the subclass: 85 | 86 | class ourRTSPClient: public RTSPClient { 87 | public: 88 | static ourRTSPClient* createNew(UsageEnvironment& env, 89 | char const* rtspURL, 90 | PyObject* frameCallback, 91 | PyObject* shutdownCallback, 92 | int clientHandle, 93 | int verbosityLevel = 0, 94 | portNumBits tunnelOverHTTPPortNum = 0); 95 | 96 | protected: 97 | ourRTSPClient(UsageEnvironment& env, char const* rtspURL, PyObject* frameCallback, PyObject* shutdownCallback, 98 | int verbosityLevel, portNumBits tunnelOverHTTPPortNum, int clientHandle); 99 | // called only by createNew(); 100 | virtual ~ourRTSPClient(); 101 | 102 | public: 103 | StreamClientState scs; 104 | }; 105 | 106 | // A function that outputs a string that identifies each stream (for debugging output). Modify this if you wish: 107 | UsageEnvironment& operator<<(UsageEnvironment& env, const RTSPClient& rtspClient) { 108 | ourRTSPClient& ourClient = (ourRTSPClient&) rtspClient; 109 | return env << "[handle:\"" << ourClient.scs.m_handle << "\"]: "; 110 | } 111 | 112 | // A function that outputs a string that identifies each subsession (for debugging output). Modify this if you wish: 113 | UsageEnvironment& operator<<(UsageEnvironment& env, const MediaSubsession& subsession) { 114 | return env << subsession.mediumName() << "/" << subsession.codecName(); 115 | } 116 | 117 | void usage(UsageEnvironment& env, char const* progName) { 118 | env << "Usage: " << progName << " ... \n"; 119 | env << "\t(where each is a \"rtsp://\" URL)\n"; 120 | } 121 | 122 | // Define a data sink (a subclass of "MediaSink") to receive the data for each subsession (i.e., each audio or video 'substream'). 123 | // In practice, this might be a class (or a chain of classes) that decodes and then renders the incoming audio or video. 124 | // Or it might be a "FileSink", for outputting the received data into a file (as is done by the "openRTSP" application). 125 | // In this example code, however, we define a simple 'dummy' sink that receives incoming data, but does nothing with it. 126 | 127 | class DummySink: public MediaSink { 128 | public: 129 | static DummySink* createNew(UsageEnvironment& env, 130 | MediaSubsession& subsession, // identifies the kind of data that's being received 131 | PyObject *frameCallback, 132 | char const* streamId = NULL, // identifies the stream itself (optional) 133 | RTSPClient *rtspClient = NULL); 134 | 135 | private: 136 | DummySink(UsageEnvironment& env, MediaSubsession& subsession, PyObject *frameCallback, char const* streamId, RTSPClient *rtspClient); 137 | // called only by "createNew()" 138 | virtual ~DummySink(); 139 | 140 | static void afterGettingFrame(void* clientData, unsigned frameSize, 141 | unsigned numTruncatedBytes, 142 | struct timeval presentationTime, 143 | unsigned durationInMicroseconds); 144 | void afterGettingFrame(unsigned frameSize, unsigned numTruncatedBytes, 145 | struct timeval presentationTime, unsigned durationInMicroseconds); 146 | 147 | private: 148 | // redefined virtual functions: 149 | virtual Boolean continuePlaying(); 150 | 151 | private: 152 | PyObject *frameCallback; 153 | u_int8_t* fReceiveBuffer; 154 | RTSPClient *fRTSPClient; 155 | MediaSubsession& fSubsession; 156 | char* fStreamId; 157 | int first; 158 | }; 159 | 160 | 161 | // Implementation of the RTSP 'response handlers': 162 | 163 | void continueAfterDESCRIBE(RTSPClient* rtspClient, int resultCode, char* resultString) { 164 | do { 165 | UsageEnvironment& env = rtspClient->envir(); // alias 166 | StreamClientState& scs = ((ourRTSPClient*)rtspClient)->scs; // alias 167 | 168 | if (resultCode != 0) { 169 | env << *rtspClient << "Failed to get a SDP description: " << resultString << "\n"; 170 | delete[] resultString; 171 | break; 172 | } 173 | 174 | char* const sdpDescription = resultString; 175 | //env << *rtspClient << "Got a SDP description:\n" << sdpDescription << "\n"; 176 | 177 | // Create a media session object from this SDP description: 178 | scs.session = MediaSession::createNew(env, sdpDescription); 179 | delete[] sdpDescription; // because we don't need it anymore 180 | if (scs.session == NULL) { 181 | env << *rtspClient << "Failed to create a MediaSession object from the SDP description: " << env.getResultMsg() << "\n"; 182 | break; 183 | } else if (!scs.session->hasSubsessions()) { 184 | env << *rtspClient << "This session has no media subsessions (i.e., no \"m=\" lines)\n"; 185 | break; 186 | } 187 | 188 | // Then, create and set up our data source objects for the session. We do this by iterating over the session's 'subsessions', 189 | // calling "MediaSubsession::initiate()", and then sending a RTSP "SETUP" command, on each one. 190 | // (Each 'subsession' will have its own data source.) 191 | scs.iter = new MediaSubsessionIterator(*scs.session); 192 | setupNextSubsession(rtspClient); 193 | return; 194 | 195 | // nocommit why have a while loop that runs only once? 196 | // there is no continue? 197 | } while (0); 198 | 199 | // An unrecoverable error occurred with this stream. 200 | shutdownStream(rtspClient); 201 | } 202 | 203 | void setupNextSubsession(RTSPClient* rtspClient) { 204 | UsageEnvironment& env = rtspClient->envir(); // alias 205 | StreamClientState& scs = ((ourRTSPClient*)rtspClient)->scs; // alias 206 | 207 | scs.subsession = scs.iter->next(); 208 | if (scs.subsession != NULL) { 209 | // Only tap the video stream (the metadata stream never 210 | // seems to send anything): 211 | //if (strcmp(scs.subsession->codecName(), "H264")) { 212 | //setupNextSubsession(rtspClient); 213 | //return; 214 | //} 215 | 216 | if (!scs.subsession->initiate()) { 217 | env << *rtspClient << "Failed to initiate the \"" << *scs.subsession << "\" subsession: " << env.getResultMsg() << "\n"; 218 | setupNextSubsession(rtspClient); // give up on this subsession; go to the next one 219 | } else { 220 | env << *rtspClient << "Initiated the \"" << *scs.subsession 221 | << "\" subsession (client ports " << scs.subsession->clientPortNum() << "-" << scs.subsession->clientPortNum()+1 << ")\n"; 222 | 223 | // Continue setting up this subsession, by sending a RTSP "SETUP" command: 224 | rtspClient->sendSetupCommand(*scs.subsession, continueAfterSETUP, False, scs.useTCP); 225 | } 226 | return; 227 | } 228 | 229 | // We've finished setting up all of the subsessions. Now, send a RTSP "PLAY" command to start the streaming: 230 | if (scs.session->absStartTime() != NULL) { 231 | // Special case: The stream is indexed by 'absolute' time, so send an appropriate "PLAY" command: 232 | rtspClient->sendPlayCommand(*scs.session, continueAfterPLAY, scs.session->absStartTime(), scs.session->absEndTime()); 233 | } else { 234 | scs.duration = scs.session->playEndTime() - scs.session->playStartTime(); 235 | rtspClient->sendPlayCommand(*scs.session, continueAfterPLAY); 236 | } 237 | } 238 | 239 | void continueAfterSETUP(RTSPClient* rtspClient, int resultCode, char* resultString) { 240 | do { 241 | UsageEnvironment& env = rtspClient->envir(); // alias 242 | StreamClientState& scs = ((ourRTSPClient*)rtspClient)->scs; // alias 243 | 244 | if (resultCode != 0) { 245 | env << *rtspClient << "Failed to set up the \"" << *scs.subsession << "\" subsession: " << resultString << "\n"; 246 | break; 247 | } 248 | 249 | env << *rtspClient << "Set up the \"" << *scs.subsession 250 | << "\" subsession (client ports " << scs.subsession->clientPortNum() << "-" << scs.subsession->clientPortNum()+1 << ")\n"; 251 | 252 | // Having successfully setup the subsession, create a data sink for it, and call "startPlaying()" on it. 253 | // (This will prepare the data sink to receive data; the actual flow of data from the client won't start happening until later, 254 | // after we've sent a RTSP "PLAY" command.) 255 | 256 | scs.subsession->sink = DummySink::createNew(env, *scs.subsession, scs.frameCallback, rtspClient->url(), rtspClient); 257 | // perhaps use your own custom "MediaSink" subclass instead 258 | if (scs.subsession->sink == NULL) { 259 | env << *rtspClient << "Failed to create a data sink for the \"" << *scs.subsession 260 | << "\" subsession: " << env.getResultMsg() << "\n"; 261 | break; 262 | } 263 | 264 | env << *rtspClient << "Created a data sink for the \"" << *scs.subsession << "\" subsession\n"; 265 | scs.subsession->miscPtr = rtspClient; // a hack to let subsession handle functions get the "RTSPClient" from the subsession 266 | scs.subsession->sink->startPlaying(*(scs.subsession->readSource()), 267 | subsessionAfterPlaying, scs.subsession); 268 | // Also set a handler to be called if a RTCP "BYE" arrives for this subsession: 269 | if (scs.subsession->rtcpInstance() != NULL) { 270 | scs.subsession->rtcpInstance()->setByeHandler(subsessionByeHandler, scs.subsession); 271 | } 272 | } while (0); 273 | delete[] resultString; 274 | 275 | // Set up the next subsession, if any: 276 | setupNextSubsession(rtspClient); 277 | } 278 | 279 | void continueAfterPLAY(RTSPClient* rtspClient, int resultCode, char* resultString) { 280 | Boolean success = False; 281 | 282 | do { 283 | UsageEnvironment& env = rtspClient->envir(); // alias 284 | StreamClientState& scs = ((ourRTSPClient*)rtspClient)->scs; // alias 285 | 286 | if (resultCode != 0) { 287 | env << *rtspClient << "Failed to start playing session: " << resultString << "\n"; 288 | break; 289 | } 290 | 291 | // Set a timer to be handled at the end of the stream's expected duration (if the stream does not already signal its end 292 | // using a RTCP "BYE"). This is optional. If, instead, you want to keep the stream active - e.g., so you can later 293 | // 'seek' back within it and do another RTSP "PLAY" - then you can omit this code. 294 | // (Alternatively, if you don't want to receive the entire stream, you could set this timer for some shorter value.) 295 | if (scs.duration > 0) { 296 | unsigned const delaySlop = 2; // number of seconds extra to delay, after the stream's expected duration. (This is optional.) 297 | scs.duration += delaySlop; 298 | unsigned uSecsToDelay = (unsigned)(scs.duration*1000000); 299 | scs.streamTimerTask = env.taskScheduler().scheduleDelayedTask(uSecsToDelay, (TaskFunc*)streamTimerHandler, rtspClient); 300 | } 301 | 302 | env << *rtspClient << "Started playing session"; 303 | if (scs.duration > 0) { 304 | env << " (for up to " << scs.duration << " seconds)"; 305 | } 306 | env << "...\n"; 307 | 308 | success = True; 309 | } while (0); 310 | delete[] resultString; 311 | 312 | if (!success) { 313 | // An unrecoverable error occurred with this stream. 314 | shutdownStream(rtspClient); 315 | } 316 | } 317 | 318 | 319 | // Implementation of the other event handlers: 320 | 321 | void subsessionAfterPlaying(void* clientData) { 322 | MediaSubsession* subsession = (MediaSubsession*)clientData; 323 | RTSPClient* rtspClient = (RTSPClient*)(subsession->miscPtr); 324 | 325 | // Begin by closing this subsession's stream: 326 | Medium::close(subsession->sink); 327 | subsession->sink = NULL; 328 | 329 | // Next, check whether *all* subsessions' streams have now been closed: 330 | MediaSession& session = subsession->parentSession(); 331 | MediaSubsessionIterator iter(session); 332 | while ((subsession = iter.next()) != NULL) { 333 | if (subsession->sink != NULL) return; // this subsession is still active 334 | } 335 | 336 | // All subsessions' streams have now been closed, so shutdown the client: 337 | shutdownStream(rtspClient); 338 | } 339 | 340 | void subsessionByeHandler(void* clientData) { 341 | MediaSubsession* subsession = (MediaSubsession*)clientData; 342 | RTSPClient* rtspClient = (RTSPClient*)subsession->miscPtr; 343 | UsageEnvironment& env = rtspClient->envir(); // alias 344 | 345 | env << *rtspClient << "Received RTCP \"BYE\" on \"" << *subsession << "\" subsession\n"; 346 | 347 | // Now act as if the subsession had closed: 348 | subsessionAfterPlaying(subsession); 349 | } 350 | 351 | void streamTimerHandler(void* clientData) { 352 | ourRTSPClient* rtspClient = (ourRTSPClient*)clientData; 353 | StreamClientState& scs = rtspClient->scs; // alias 354 | 355 | scs.streamTimerTask = NULL; 356 | 357 | // Shut down the stream: 358 | shutdownStream(rtspClient); 359 | } 360 | 361 | void shutdownStream(RTSPClient* rtspClient, int exitCode) { 362 | // fprintf(stderr, "shutting down, getting environment\n"); 363 | 364 | UsageEnvironment& env = rtspClient->envir(); // alias 365 | // fprintf(stderr, "shutting down, getting client state\n"); 366 | StreamClientState& scs = ((ourRTSPClient*)rtspClient)->scs; // alias 367 | // fprintf (stderr, "Got scs, m_handle: %d\n", scs.m_handle); 368 | 369 | env << *rtspClient << "in close stream\n"; 370 | // First, check whether any subsessions have still to be closed: 371 | if (scs.session != NULL) { 372 | Boolean someSubsessionsWereActive = False; 373 | MediaSubsessionIterator iter(*scs.session); 374 | MediaSubsession* subsession; 375 | 376 | while ((subsession = iter.next()) != NULL) { 377 | if (subsession->sink != NULL) { 378 | Medium::close(subsession->sink); 379 | subsession->sink = NULL; 380 | 381 | if (subsession->rtcpInstance() != NULL) { 382 | // nocommit 383 | //subsession->rtcpInstance()->setByeHandler(NULL, NULL); // in case the server sends a RTCP "BYE" while handling "TEARDOWN" 384 | } 385 | 386 | someSubsessionsWereActive = True; 387 | } 388 | } 389 | 390 | if (someSubsessionsWereActive) { 391 | // Send a RTSP "TEARDOWN" command, to tell the server to shutdown the stream. 392 | // Don't bother handling the response to the 393 | // "TEARDOWN". 394 | rtspClient->sendTeardownCommand(*scs.session, NULL); 395 | } 396 | } 397 | 398 | // Put these into local variables before they get reclaimed by Medium::close() 399 | int handle = scs.m_handle; 400 | PyObject* shutdownCallback = scs.shutdownCallback; 401 | Medium::close(rtspClient); 402 | // Note that this will also cause this stream's "StreamClientState" structure to get reclaimed. 403 | clientList[handle] = NULL; 404 | /* This callback will work at some point in the future. Currently, though this callback triggers: https://bugs.python.org/issue23571 405 | Which generates a SystemError on stopEventLoop. This kills the interpreter, which is a wholly bad outcome. 406 | THEREFORE, I am leaving the code here, despite its lack of goodness for now. 407 | 408 | if (shutdownCallback != NULL) { 409 | // Note that the GIL lock is required because this method is frequently called from the delayed babysitter thread. 410 | PyGILState_STATE gstate; 411 | fprintf(stderr, "doing shutdowncallback\n"); 412 | gstate = PyGILState_Ensure(); 413 | PyEval_CallFunction(shutdownCallback, ""); 414 | PyGILState_Release(gstate); 415 | } 416 | */ 417 | } 418 | 419 | 420 | // Implementation of "ourRTSPClient": 421 | 422 | ourRTSPClient* ourRTSPClient::createNew(UsageEnvironment& env, char const* rtspURL, PyObject* frameCallback, PyObject* shutdownCallback, 423 | int clientHandle, 424 | int verbosityLevel, portNumBits tunnelOverHTTPPortNum) { 425 | ourRTSPClient* result = new ourRTSPClient(env, rtspURL, frameCallback, shutdownCallback, verbosityLevel, tunnelOverHTTPPortNum, clientHandle); 426 | return result; 427 | } 428 | 429 | ourRTSPClient::ourRTSPClient(UsageEnvironment& env, char const* rtspURL, PyObject* frameCallback, PyObject* shutdownCallback, 430 | int verbosityLevel, portNumBits tunnelOverHTTPPortNum, int clientHandle) 431 | : RTSPClient(env, rtspURL, verbosityLevel, "", tunnelOverHTTPPortNum, -1) { 432 | Py_INCREF(frameCallback); 433 | scs.frameCallback = frameCallback; 434 | scs.shutdownCallback = shutdownCallback; 435 | scs.m_handle = clientHandle; 436 | } 437 | 438 | ourRTSPClient::~ourRTSPClient() { 439 | } 440 | 441 | 442 | // Implementation of "StreamClientState": 443 | 444 | StreamClientState::StreamClientState() 445 | : iter(NULL), session(NULL), subsession(NULL), streamTimerTask(NULL), duration(0.0) { 446 | } 447 | 448 | StreamClientState::~StreamClientState() { 449 | delete iter; 450 | if (session != NULL) { 451 | // We also need to delete "session", and unschedule "streamTimerTask" (if set) 452 | UsageEnvironment& env = session->envir(); // alias 453 | 454 | env.taskScheduler().unscheduleDelayedTask(streamTimerTask); 455 | Medium::close(session); 456 | } 457 | } 458 | 459 | 460 | // Implementation of "DummySink": 461 | 462 | // Define the size of the buffer that we'll use: 463 | #define DUMMY_SINK_RECEIVE_BUFFER_SIZE 1024*1024 464 | 465 | DummySink* DummySink::createNew(UsageEnvironment& env, MediaSubsession& subsession, PyObject *frameCallback, char const* streamId, RTSPClient *rtspClient) { 466 | return new DummySink(env, subsession, frameCallback, streamId, rtspClient); 467 | } 468 | 469 | DummySink::DummySink(UsageEnvironment& env, MediaSubsession& subsession, PyObject *frameCallbackIn, char const* streamId, RTSPClient *rtspClient) 470 | : MediaSink(env), 471 | fSubsession(subsession) { 472 | fStreamId = strDup(streamId); 473 | fReceiveBuffer = new u_int8_t[DUMMY_SINK_RECEIVE_BUFFER_SIZE]; 474 | frameCallback = frameCallbackIn; 475 | fRTSPClient = rtspClient; 476 | first = 1; 477 | } 478 | 479 | DummySink::~DummySink() { 480 | delete[] fReceiveBuffer; 481 | delete[] fStreamId; 482 | Py_DECREF(frameCallback); 483 | } 484 | 485 | void DummySink::afterGettingFrame(void* clientData, unsigned frameSize, unsigned numTruncatedBytes, 486 | struct timeval presentationTime, unsigned durationInMicroseconds) { 487 | DummySink* sink = (DummySink*)clientData; 488 | sink->afterGettingFrame(frameSize, numTruncatedBytes, presentationTime, durationInMicroseconds); 489 | } 490 | 491 | void DummySink::afterGettingFrame(unsigned frameSize, unsigned numTruncatedBytes, 492 | struct timeval presentationTime, unsigned durationInUS) { 493 | PyEval_RestoreThread(threadState); 494 | if (first == 1) { 495 | // NOTE: only necessary for H264 I think? 496 | unsigned numSPropRecords; 497 | SPropRecord* sPropRecords = parseSPropParameterSets(fSubsession.fmtp_spropparametersets(), numSPropRecords); 498 | for(unsigned i=0;istopGettingFrames(); 504 | env->taskScheduler().scheduleDelayedTask(0, (TaskFunc*)shutdownStream, fRTSPClient); 505 | PyEval_SaveThread(); 506 | return; 507 | //break; 508 | } 509 | } 510 | delete[] sPropRecords; 511 | first = 0; 512 | } 513 | 514 | // TODO: can we somehow avoid ... making a full copy here: 515 | //printf("%d bytes\n", frameSize);fflush(stdout); 516 | PyObject *result = PyEval_CallFunction(frameCallback, "sy#llI", fSubsession.codecName(), fReceiveBuffer, frameSize, presentationTime.tv_sec, presentationTime.tv_usec, durationInUS); 517 | if (result == NULL) { 518 | fprintf(stderr, "Exception in RTSP callback:"); 519 | PyErr_PrintEx(1); 520 | fSource->stopGettingFrames(); 521 | env->taskScheduler().scheduleDelayedTask(0, (TaskFunc*)shutdownStream, fRTSPClient); 522 | PyEval_SaveThread(); 523 | return; 524 | } 525 | PyEval_SaveThread(); 526 | 527 | // We've just received a frame of data. (Optionally) print out information about it: 528 | #ifdef DEBUG_PRINT_EACH_RECEIVED_FRAME 529 | if (fStreamId != NULL) envir() << "Stream \"" << fStreamId << "\"; "; 530 | envir() << fSubsession.mediumName() << "/" << fSubsession.codecName() << ":\tReceived " << frameSize << " bytes"; 531 | if (numTruncatedBytes > 0) envir() << " (with " << numTruncatedBytes << " bytes truncated)"; 532 | char uSecsStr[6+1]; // used to output the 'microseconds' part of the presentation time 533 | sprintf(uSecsStr, "%06u", (unsigned)presentationTime.tv_usec); 534 | envir() << ".\tPresentation time: " << (int)presentationTime.tv_sec << "." << uSecsStr; 535 | if (fSubsession.rtpSource() != NULL && !fSubsession.rtpSource()->hasBeenSynchronizedUsingRTCP()) { 536 | envir() << "!"; // mark the debugging output to indicate that this presentation time is not RTCP-synchronized 537 | } 538 | #ifdef DEBUG_PRINT_NPT 539 | envir() << "\tNPT: " << fSubsession.getNormalPlayTime(presentationTime); 540 | #endif 541 | envir() << "\n"; 542 | #endif 543 | 544 | // Then continue, to request the next frame of data: 545 | continuePlaying(); 546 | } 547 | 548 | Boolean DummySink::continuePlaying() { 549 | if (fSource == NULL) return False; // sanity check (should not happen) 550 | 551 | // Request the next frame of data from our input source. "afterGettingFrame()" will get called later, when it arrives: 552 | fSource->getNextFrame(fReceiveBuffer, DUMMY_SINK_RECEIVE_BUFFER_SIZE, 553 | afterGettingFrame, this, 554 | onSourceClosure, this); 555 | return True; 556 | } 557 | 558 | static PyObject * 559 | startRTSP(PyObject *self, PyObject *args) 560 | { 561 | const char *rtspURL; 562 | PyObject *frameCallback; 563 | PyObject *shutdownCallback; 564 | int useTCP = 1; 565 | 566 | if (!PyArg_ParseTuple(args, "sOO|i", &rtspURL, &frameCallback, &shutdownCallback, &useTCP)) { 567 | return NULL; 568 | } 569 | 570 | if (!PyCallable_Check(frameCallback)) { 571 | PyErr_SetString(error, "frame callback must be a callable"); 572 | return NULL; 573 | } 574 | 575 | if (!PyCallable_Check(shutdownCallback)) { 576 | PyErr_SetString(error, "shutdown callback must be a callable"); 577 | return NULL; 578 | } 579 | 580 | // find the right index -- this has a race condition. 581 | int clientHandle = -1; 582 | int i; 583 | for (i=last_handle + 1; iscs.useTCP = useTCP != 0; 618 | 619 | // Next, send a RTSP "DESCRIBE" command, to get a SDP description for the stream. 620 | // Note that this command - like all RTSP commands - is sent asynchronously; we do not block, waiting for a response. 621 | // Instead, the following function call returns immediately, and we handle the RTSP response later, from within the event loop: 622 | rtspClient->sendDescribeCommand(continueAfterDESCRIBE); 623 | 624 | Py_INCREF(Py_None); 625 | // fprintf(stderr, "returning handle %d and scs.m_handle is %d\n", clientHandle, rtspClient->scs.m_handle); 626 | return Py_BuildValue("i", clientHandle); 627 | } 628 | 629 | static PyObject * 630 | stopRTSP(PyObject *self, PyObject *args) 631 | { 632 | int rtspClientHandle = 1; 633 | 634 | if (!PyArg_ParseTuple(args, "i", &rtspClientHandle)) { 635 | PyErr_SetString(error, "Invalid arguments"); 636 | return NULL; 637 | } 638 | 639 | char buffer[50]; 640 | if (rtspClientHandle >= MAX_CLIENTS || rtspClientHandle < 0) { 641 | sprintf(buffer, "Invalid handle argument %d", rtspClientHandle); 642 | PyErr_SetString(error, buffer); 643 | return NULL; 644 | } 645 | if (clientList[rtspClientHandle] == NULL) { 646 | sprintf(buffer, "Invalid null handle %d", rtspClientHandle); 647 | PyErr_SetString(error, buffer); 648 | return NULL; 649 | } 650 | 651 | if (clientList[rtspClientHandle] == (RTSPClient*) -1) { 652 | sprintf(buffer, "Invalid handle %d", rtspClientHandle); 653 | PyErr_SetString(error, buffer); 654 | return NULL; 655 | } 656 | 657 | RTSPClient* client; 658 | client = clientList[rtspClientHandle]; 659 | if (client == NULL) { 660 | sprintf(buffer, "Invalid null handle %d", rtspClientHandle); 661 | PyErr_SetString(error, buffer); 662 | return NULL; 663 | } 664 | clientList[rtspClientHandle] = NULL; 665 | shutdownStream(client); 666 | 667 | Py_INCREF(Py_None); 668 | return Py_None; 669 | } 670 | 671 | static char stopEventLoopFlag = 0; 672 | 673 | static PyObject * 674 | runEventLoop(PyObject *self, PyObject *args) 675 | { 676 | stopEventLoopFlag = 0; 677 | 678 | // All subsequent activity takes place within the event loop: 679 | threadState = PyEval_SaveThread(); 680 | env->taskScheduler().doEventLoop(&stopEventLoopFlag); 681 | PyEval_RestoreThread(threadState); 682 | 683 | Py_INCREF(Py_None); 684 | return Py_None; 685 | } 686 | 687 | static PyObject * 688 | stopEventLoop(PyObject *self, PyObject *args) 689 | { 690 | stopEventLoopFlag = 1; 691 | 692 | Py_INCREF(Py_None); 693 | return Py_None; 694 | } 695 | 696 | static PyMethodDef moduleMethods[] = { 697 | {"startRTSP", startRTSP, METH_VARARGS, "Start loading frames from the provided RTSP url. First argument is the URL string (should be rtsp://username:password@host/...; second argument is a callback function called once per received frame; third agument is the callback function to be called if/when the stream is shut down; fourth is False if UDP transport should be used and True if TCP transport should be used."}, 698 | {"stopRTSP", stopRTSP, METH_VARARGS, "Stop loading frames from the provided RTSP url. First argument is the int of the RTSP handler. This is the same int that was returned by startRTSP"}, 699 | {"runEventLoop", runEventLoop, METH_NOARGS, "Run the event loop."}, 700 | {"stopEventLoop", stopEventLoop, METH_NOARGS, "Stop the event loop, which will cause runEventLoop (in another thread) to stop and return."}, 701 | {NULL, NULL, 0, NULL} /* Sentinel */ 702 | }; 703 | 704 | static struct PyModuleDef module = { 705 | PyModuleDef_HEAD_INIT, 706 | "live555", /* name of module */ 707 | NULL, /* module documentation, may be NULL */ 708 | -1, /* size of per-interpreter state of the module, 709 | or -1 if the module keeps state in global variables. */ 710 | moduleMethods 711 | }; 712 | 713 | PyMODINIT_FUNC 714 | PyInit_live555(void) 715 | { 716 | PyObject *m; 717 | 718 | m = PyModule_Create(&module); 719 | if (m == NULL) { 720 | return NULL; 721 | } 722 | 723 | error = PyErr_NewException("live555.error", NULL, NULL); 724 | Py_INCREF(error); 725 | PyModule_AddObject(m, "error", error); 726 | 727 | // Begin by setting up our usage environment: 728 | scheduler = BasicTaskScheduler::createNew(); 729 | env = BasicUsageEnvironment::createNew(*scheduler); 730 | 731 | return m; 732 | } 733 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup, Extension 2 | 3 | INSTALL_DIR = './live' 4 | module = Extension('live555', 5 | include_dirs=['%s/%s/include' % (INSTALL_DIR, x) for x in ['liveMedia', 'BasicUsageEnvironment', 'UsageEnvironment', 'groupsock']], 6 | libraries=['liveMedia', 'groupsock', 'BasicUsageEnvironment', 'UsageEnvironment'], 7 | #extra_compile_args = ['-fPIC'], 8 | library_dirs=['%s/%s' % (INSTALL_DIR, x) for x in ['liveMedia', 'UsageEnvironment', 'BasicUsageEnvironment','groupsock']], 9 | sources = ['module.cpp']) 10 | 11 | setup(name = 'live555', 12 | version = '1.0', 13 | description = 'Basic wrapper around live555 to load RTSP video streams', 14 | ext_modules = [module]) 15 | --------------------------------------------------------------------------------