port to new iTSMPEGDecoder api
[enigma2-plugins.git] / partnerbox / src / servicewebts / servicewebts.cpp
1
2 /*******************************************************************************
3  VLC Player Plugin by A. Lätsch 2007
4
5  Modified by Dr. Best
6
7  This is free software; you can redistribute it and/or modify it under
8  the terms of the GNU General Public License as published by the Free
9  Software Foundation; either version 2, or (at your option) any later
10  version.
11 ********************************************************************************/
12
13 #include <sys/types.h>
14 #include <sys/stat.h>
15 #include <fcntl.h>
16 #include <string>
17 #include <sys/socket.h>
18 #include <netdb.h>
19 #include <signal.h>
20 #include <time.h>
21 #include "servicewebts.h"
22 #include <lib/base/eerror.h>
23 #include <lib/base/object.h>
24 #include <lib/base/ebase.h>
25 #include <lib/service/service.h>
26 #include <lib/base/init_num.h>
27 #include <lib/base/init.h>
28 #include <lib/dvb/decoder.h>
29
30 #include <lib/dvb/pmt.h>
31
32 #define MAX(a,b) ((a) > (b) ? (a) : (b))
33
34 int VPID = 0;
35 int PID_SET = 0;
36 int APID = 0;
37
38
39 /********************************************************************/
40 /* eServiceFactoryWebTS                                                */
41 /********************************************************************/
42
43 eServiceFactoryWebTS::eServiceFactoryWebTS()
44 {
45         ePtr<eServiceCenter> sc;
46
47         eServiceCenter::getPrivInstance(sc);
48         if (sc)
49         {
50                 std::list<std::string> extensions;
51                 sc->addServiceFactory(eServiceFactoryWebTS::id, this, extensions);
52         }
53 }
54
55 eServiceFactoryWebTS::~eServiceFactoryWebTS()
56 {
57         ePtr<eServiceCenter> sc;
58
59         eServiceCenter::getPrivInstance(sc);
60         if (sc)
61                 sc->removeServiceFactory(eServiceFactoryWebTS::id);
62 }
63
64 DEFINE_REF(eServiceFactoryWebTS)
65
66 // iServiceHandler
67 RESULT eServiceFactoryWebTS::play(const eServiceReference &ref, ePtr<iPlayableService> &ptr)
68 {
69         ptr = new eServiceWebTS(ref);
70         return 0;
71 }
72
73 RESULT eServiceFactoryWebTS::record(const eServiceReference &ref, ePtr<iRecordableService> &ptr)
74 {
75         ptr=0;
76         return -1;
77 }
78
79 RESULT eServiceFactoryWebTS::list(const eServiceReference &, ePtr<iListableService> &ptr)
80 {
81         ptr=0;
82         return -1;
83 }
84
85 RESULT eServiceFactoryWebTS::info(const eServiceReference &ref, ePtr<iStaticServiceInformation> &ptr)
86 {
87         ptr = 0;
88         return -1;
89 }
90
91 RESULT eServiceFactoryWebTS::offlineOperations(const eServiceReference &, ePtr<iServiceOfflineOperations> &ptr)
92 {
93         ptr = 0;
94         return -1;
95 }
96
97
98 /********************************************************************/
99 /* TSAudioInfoWeb                                            */
100 /********************************************************************/
101 DEFINE_REF(TSAudioInfoWeb);
102
103 void TSAudioInfoWeb::addAudio(int pid, std::string lang, std::string desc, int type) {
104         StreamInfo as;
105         as.description = desc;
106         as.language = lang;
107         as.pid = pid;
108         as.type = type;
109         audioStreams.push_back(as);
110 }
111
112
113 /********************************************************************/
114 /* eServiceWebTS                                                       */
115 /********************************************************************/
116
117 eServiceWebTS::eServiceWebTS(const eServiceReference &url): m_pump(eApp, 1)
118 {
119         eDebug("ServiceWebTS construct!");
120         m_filename = url.path.c_str();
121         m_vpid = url.getData(0) == 0 ? 0x44 : url.getData(0);
122         m_apid = url.getData(1) == 0 ? 0x45 : url.getData(1);
123         m_state = stIdle;
124         m_audioInfo = 0;
125 }
126
127 eServiceWebTS::~eServiceWebTS()
128 {
129         eDebug("ServiceWebTS destruct!");
130         if (m_state == stRunning)
131                 stop();
132 }
133
134 DEFINE_REF(eServiceWebTS);
135
136 size_t crop(char *buf)
137 {
138         size_t len = strlen(buf) - 1;
139         while (len > 0 && (buf[len] == '\r' || buf[len] == '\n')) {
140                 buf[len--] = '\0';
141         }
142         return len;
143 }
144
145 static int getline(char** pbuffer, size_t* pbufsize, int fd)
146 {
147         size_t i = 0;
148         int rc;
149         while (true) {
150                 if (i >= *pbufsize) {
151                         char *newbuf = (char*)realloc(*pbuffer, (*pbufsize)+1024);
152                         if (newbuf == NULL)
153                                 return -ENOMEM;
154                         *pbuffer = newbuf;
155                         *pbufsize = (*pbufsize)+1024;
156                 }
157                 rc = ::read(fd, (*pbuffer)+i, 1);
158                 if (rc <= 0 || (*pbuffer)[i] == '\n')
159                 {
160                         (*pbuffer)[i] = '\0';
161                         return rc <= 0 ? -1 : i;
162                 }
163                 if ((*pbuffer)[i] != '\r') i++;
164         }
165 }
166
167
168
169 int eServiceWebTS::openHttpConnection(std::string url)
170 {
171         std::string host;
172         int port = 80;
173         std::string uri;
174
175         int slash = url.find("/", 7);
176         if (slash > 0) {
177                 host = url.substr(7, slash-7);
178                 uri = url.substr(slash, url.length()-slash);
179         } else {
180                 host = url.substr(7, url.length()-7);
181                 uri = "";
182         }
183         int dp = host.find(":");
184         if (dp == 0) {
185                 port = atoi(host.substr(1, host.length()-1).c_str());
186                 host = "localhost";
187         } else if (dp > 0) {
188                 port = atoi(host.substr(dp+1, host.length()-dp-1).c_str());
189                 host = host.substr(0, dp);
190         }
191
192         struct hostent* h = gethostbyname(host.c_str());
193         if (h == NULL || h->h_addr_list == NULL)
194                 return -1;
195         int fd = socket(PF_INET, SOCK_STREAM, 0);
196         if (fd == -1)
197                 return -1;
198
199         struct sockaddr_in addr;
200         addr.sin_family = AF_INET;
201         addr.sin_addr.s_addr = *((in_addr_t*)h->h_addr_list[0]);
202         addr.sin_port = htons(port);
203
204         eDebug("connecting to %s", url.c_str());
205
206         if (connect(fd, (sockaddr*)&addr, sizeof(addr)) == -1) {
207                 std::string msg = "connect failed for: " + url;
208                 eDebug(msg.c_str());
209                 return -1;
210         }
211
212         std::string request = "GET ";
213         request.append(uri).append(" HTTP/1.1\n");
214         request.append("Host: ").append(host).append("\n");
215         request.append("Accept: */*\n");
216         request.append("Connection: close\n");
217         request.append("\n");
218         //eDebug(request.c_str());
219         write(fd, request.c_str(), request.length());
220
221         int rc;
222         size_t buflen = 1000;
223         char* linebuf = (char*)malloc(1000);
224
225         rc = getline(&linebuf, &buflen, fd);
226         //eDebug("RECV(%d): %s", rc, linebuf);
227         if (rc <= 0)
228         {
229                 close(fd);
230                 free(linebuf);
231                 return -1;
232         }
233
234         char proto[100];
235         int statuscode = 0;
236         char statusmsg[100];
237         rc = sscanf(linebuf, "%99s %d %99s", proto, &statuscode, statusmsg);
238         if (rc != 3 || statuscode != 200) {
239                 eDebug("wrong response: \"200 OK\" expected.");
240                 free(linebuf);
241                 close(fd);
242                 return -1;
243         }
244         eDebug("proto=%s, code=%d, msg=%s", proto, statuscode, statusmsg);
245         while (rc > 0)
246         {
247                 rc = getline(&linebuf, &buflen, fd);
248                 //eDebug("RECV(%d): %s", rc, linebuf);
249         }
250         free(linebuf);
251
252         return fd;
253 }
254
255 RESULT eServiceWebTS::connectEvent(const Slot2<void,iPlayableService*,int> &event, ePtr<eConnection> &connection)
256 {
257         connection = new eConnection((iPlayableService*)this, m_event.connect(event));
258         return 0;
259 }
260
261 RESULT eServiceWebTS::start()
262 {
263         ePtr<eDVBResourceManager> rmgr;
264         eDVBResourceManager::getInstance(rmgr);
265         eDVBChannel dvbChannel(rmgr, 0);
266         if (dvbChannel.getDemux(m_decodedemux, iDVBChannel::capDecode) != 0) {
267                 eDebug("Cannot allocate decode-demux");
268                 return 1;
269         }
270         if (m_decodedemux->getMPEGDecoder(m_decoder, 1) != 0) {
271                 eDebug("Cannot allocate MPEGDecoder");
272                 return 1;
273         }
274         //m_decoder->setVideoPID(m_vpid, eDVBVideo::MPEG2);
275         //m_decoder->setAudioPID(m_apid, eDVBAudio::aMPEG);
276         m_streamthread = new eStreamThreadWeb();
277         CONNECT(m_streamthread->m_event, eServiceWebTS::recv_event);
278         //m_decoder->freeze(0);
279         //m_decoder->preroll();
280         if (unpause() != 0) return -1;
281         //m_state = stRunning;
282         //m_event(this, evStart);
283         return 0;
284 }
285
286 RESULT eServiceWebTS::stop()
287 {
288         if (m_state != stRunning)
289                 return -1;
290         printf("TS: %s stop\n", m_filename.c_str());
291         m_streamthread->stop();
292         m_decodedemux->flush();
293         m_state = stStopped;
294         m_audioInfo = 0;
295         APID = 0;
296         VPID = 0;
297         PID_SET = 0;
298         return 0;
299 }
300
301 void eServiceWebTS::recv_event(int evt)
302 {
303         eDebug("eServiceWebTS::recv_event: %d", evt);
304         switch (evt) {
305         case eStreamThreadWeb::evtEOS:
306                 m_decodedemux->flush();
307                 m_state = stStopped;
308                 m_event((iPlayableService*)this, evEOF);
309                 break;
310         case eStreamThreadWeb::evtReadError:
311         case eStreamThreadWeb::evtWriteError:
312                 m_decoder->pause();
313                 m_state = stStopped;
314                 m_event((iPlayableService*)this, evEOF);
315                 break;
316         case eStreamThreadWeb::evtSOS:
317                 m_event((iPlayableService*)this, evSOF);
318                 break;
319         case eStreamThreadWeb::evtStreamInfo:
320                 if (VPID != 0 && PID_SET == 0 && APID != 0)
321                 {
322                         PID_SET = 1;
323                         m_decodedemux->flush();
324                         m_decoder->setVideoPID(VPID, eDVBVideo::MPEG2);
325                         m_decoder->setAudioPID(APID, eDVBAudio::aMPEG);
326                         m_decoder->pause();
327                         m_state = stRunning;
328                         m_event(this, evStart);
329                         m_decoder->play();
330                         
331                 }       
332                 bool wasnull = !m_audioInfo;
333                 m_streamthread->getAudioInfo(m_audioInfo);
334                 //if (m_audioInfo)
335                 //      eDebug("[ServiceWebTS] %d audiostreams found", m_audioInfo->audioStreams.size());
336                 if (m_audioInfo && wasnull) {
337                         eDebug("[ServiceWebTS] %d audiostreams found", m_audioInfo->audioStreams.size());
338                         int sel = getCurrentTrack();
339                         if (sel < 0)
340                                 selectTrack(0);
341                         else if (m_audioInfo->audioStreams[sel].type != eDVBAudio::aMPEG)
342                                 selectTrack(sel);
343                 }
344                 break;
345         }
346 }
347
348 RESULT eServiceWebTS::pause(ePtr<iPauseableService> &ptr)
349 {
350         ptr = this;
351         return 0;
352 }
353
354 // iPausableService
355 RESULT eServiceWebTS::pause()
356 {
357         m_streamthread->stop();
358         m_decoder->pause();
359         return 0;
360 }
361
362 RESULT eServiceWebTS::unpause()
363 {
364         int is_streaming = !strncmp(m_filename.c_str(), "http://", 7);
365         int srcfd = -1;
366         if (is_streaming) {
367                 srcfd = openHttpConnection(m_filename);
368         } else {
369                 srcfd = ::open(m_filename.c_str(), O_RDONLY);
370         }
371         if (srcfd < 0) {
372                 eDebug("Cannot open source stream: %s", m_filename.c_str());
373                 return 1;
374         }
375
376         int destfd = ::open("/dev/misc/pvr", O_WRONLY);
377         if (destfd < 0) {
378                 eDebug("Cannot open /dev/misc/pvr");
379                 ::close(srcfd);
380                 return 1;
381         }
382         //m_decodedemux->flush();
383         m_streamthread->start(srcfd, destfd);
384         //m_decoder->unfreeze();
385         return 0;
386 }
387
388 // iSeekableService
389 RESULT eServiceWebTS::seek(ePtr<iSeekableService> &ptr)
390 {
391         ptr = this;
392         return 0;
393 }
394
395 RESULT eServiceWebTS::getLength(pts_t &pts)
396 {
397         return 0;
398 }
399
400 RESULT eServiceWebTS::seekTo(pts_t to)
401 {
402         return 0;
403 }
404
405 RESULT eServiceWebTS::seekRelative(int direction, pts_t to)
406 {
407         return 0;
408 }
409
410 RESULT eServiceWebTS::getPlayPosition(pts_t &pts)
411 {
412         return 0;
413 }
414
415 RESULT eServiceWebTS::setTrickmode(int trick)
416 {
417         return -1;
418 }
419
420 RESULT eServiceWebTS::isCurrentlySeekable()
421 {
422         return 1;
423 }
424
425 RESULT eServiceWebTS::info(ePtr<iServiceInformation>&i)
426 {
427         i = this;
428         return 0;
429 }
430
431 RESULT eServiceWebTS::getName(std::string &name)
432 {
433         name = m_filename;
434         size_t n = name.rfind('/');
435         if (n != std::string::npos)
436                 name = name.substr(n + 1);
437         return 0;
438 }
439
440 int eServiceWebTS::getInfo(int w)
441 {
442         return resNA;
443 }
444
445 std::string eServiceWebTS::getInfoString(int w)
446 {
447         return "";
448 }
449
450 int eServiceWebTS::getNumberOfTracks() {
451         if (m_audioInfo)
452                 return (int)m_audioInfo->audioStreams.size();
453         else
454                 return 0;
455 }
456
457 RESULT eServiceWebTS::selectTrack(unsigned int i) {
458         if (m_audioInfo) {
459                 m_apid = m_audioInfo->audioStreams[i].pid;
460                 eDebug("[ServiceWebTS] audio track %d PID 0x%02x type %d\n", i, m_apid, m_audioInfo->audioStreams[i].type);
461                 m_decoder->setAudioPID(m_apid, m_audioInfo->audioStreams[i].type);
462                 if (m_state == stRunning)
463                         m_decoder->set();
464                 return 0;
465         } else {
466                 return -1;
467         }
468 }
469
470 RESULT eServiceWebTS::getTrackInfo(struct iAudioTrackInfo &info, unsigned int n) {
471         if (m_audioInfo) {
472                 info.m_pid = m_audioInfo->audioStreams[n].pid;
473                 info.m_description = m_audioInfo->audioStreams[n].description;
474                 info.m_language = m_audioInfo->audioStreams[n].language;
475                 return 0;
476         } else {
477                 return -1;
478         }
479 }
480
481 int eServiceWebTS::getCurrentTrack() {
482         if (m_audioInfo) {
483                 for (size_t i = 0; i < m_audioInfo->audioStreams.size(); i++) {
484                         if (m_apid == m_audioInfo->audioStreams[i].pid) {
485                                 return i;
486                         }
487                 }
488         }
489         return -1;
490 }
491
492 /********************************************************************/
493 /* eStreamThreadWeb                                                       */
494 /********************************************************************/
495
496 DEFINE_REF(eStreamThreadWeb)
497
498 eStreamThreadWeb::eStreamThreadWeb(): m_messagepump(eApp, 0) {
499         CONNECT(m_messagepump.recv_msg, eStreamThreadWeb::recvEvent);
500 }
501 eStreamThreadWeb::~eStreamThreadWeb() {
502 }
503
504 void eStreamThreadWeb::start(int srcfd, int destfd) {
505         m_srcfd = srcfd;
506         m_destfd = destfd;
507         m_stop = false;
508         m_audioInfo = 0;
509         run(IOPRIO_CLASS_RT);
510 }
511 void eStreamThreadWeb::stop() {
512         m_stop = true;
513         kill();
514 }
515
516 void eStreamThreadWeb::recvEvent(const int &evt)
517 {
518         m_event(evt);
519 }
520
521 RESULT eStreamThreadWeb::getAudioInfo(ePtr<TSAudioInfoWeb> &ptr)
522 {
523         ptr = m_audioInfo;
524         return 0;
525 }
526
527 #define REGISTRATION_DESCRIPTOR 5
528 #define LANGUAGE_DESCRIPTOR 10
529
530 std::string eStreamThreadWeb::getDescriptor(unsigned char buf[], int buflen, int type)
531 {
532         int desc_len;
533         while (buflen > 1) {
534                 desc_len = buf[1];
535                 if (buf[0] == type) {
536                         char str[21];
537                         if (desc_len > 20) desc_len = 20;
538                         strncpy(str, (char*)buf+2, desc_len);
539                         str[desc_len] = '\0';
540                         return std::string(str);
541                 } else {
542                         buflen -= desc_len+2;
543                         buf += desc_len+2;
544                 }
545         }
546         return "";
547 }
548
549 bool eStreamThreadWeb::scanAudioInfo(unsigned char buf[], int len)
550 {
551         if (len < 1880)
552                 return false;
553
554         int adaptfield, pmtpid, offset;
555         unsigned char pmt[1188];
556         int pmtsize = 0;
557
558         for (int a=0; a < len - 188*4; a++) {
559                 if ( buf[a] != 0x47 || buf[a + 188] != 0x47 || buf[a + 376] != 0x47 )
560                         continue; // TS Header
561
562                 if ((0x40 & buf[a + 1]) == 0) // start
563                         continue;
564
565                 if ((0xC0 & buf[a + 3]) != 0) // scrambling
566                         continue;
567
568                 adaptfield = (0x30 & buf[a + 3]) >> 4;
569
570                 if ((adaptfield & 1) == 0) // adapt - no payload
571                         continue;
572
573                 offset = adaptfield == 3 ? 1 + (0xFF & buf[a + 4]) : 0; //adaptlength
574
575                 if (buf[a + offset + 4] != 0 || buf[a + offset + 5] != 2 || (0xF0 & buf[a + offset + 6]) != 0xB0)
576                 {
577                         a += 187;
578                         continue;
579                 }
580
581                 pmtpid = (0x1F & buf[a + 1])<<8 | (0xFF & buf[a + 2]);
582                 memcpy(pmt + pmtsize, buf + a + 4 + offset, 184 - offset);
583                 pmtsize += 184 - offset;
584
585                 if (pmtsize >= 1000)
586                         break;
587         }
588
589         if (pmtsize == 0) return false;
590
591         int pmtlen = (0x0F & pmt[2]) << 8 | (0xFF & pmt[3]);
592         std::string lang;
593         std::string pd_type;
594         ePtr<TSAudioInfoWeb> ainfo = new TSAudioInfoWeb();
595
596         for (int b=8; b < pmtlen-4 && b < pmtsize-6; b++)
597         {
598                 if ( (0xe0 & pmt[b+1]) != 0xe0 )
599                         continue;
600
601                 int pid = (0x1F & pmt[b+1])<<8 | (0xFF & pmt[b+2]);
602                 switch(pmt[b])
603                 {
604                 case 1:
605                 case 2: // MPEG Video
606                         //addVideo(pid, "MPEG2");
607                         if (VPID == 0)
608                                 VPID= pid;
609                         break;
610
611                 case 0x1B: // H.264 Video
612                         //addVideo(pid, "H.264");
613                         break;
614                 case 3:
615                 case 4: // MPEG Audio
616                         if (APID == 0)
617                                 APID =pid;
618                         lang = getDescriptor(pmt+b+5, pmt[b+4], LANGUAGE_DESCRIPTOR);
619                         ainfo->addAudio(pid, lang, "MPEG", eDVBAudio::aMPEG);
620                         break;
621
622                 case 0x80:
623                 case 0x81:  //private data of AC3 in ATSC
624                 case 0x82:
625                 case 0x83:
626                 case 6:
627                         lang = getDescriptor(pmt+b+5, pmt[b+4], LANGUAGE_DESCRIPTOR);
628                         pd_type = getDescriptor(pmt+b+5, pmt[b+4], REGISTRATION_DESCRIPTOR);
629                         //if (pd_type == "AC-3")
630                         // dirty dirty :-) Aber es funktioniert...
631                         if (lang.length() != 0)
632                                 ainfo->addAudio(pid, lang, "AC-3", eDVBAudio::aAC3);
633                         break;
634                 }
635                 b += 4 + pmt[b+4];
636         }
637         if (ainfo->audioStreams.size() > 0) {
638                 m_audioInfo = ainfo;
639                 return true;
640         } else {
641                 return false;
642         }
643 }
644
645 void eStreamThreadWeb::thread() {
646         const int bufsize = 40000;
647         unsigned char buf[bufsize];
648         bool eof = false;
649         fd_set rfds;
650         fd_set wfds;
651         struct timeval timeout;
652         int rc,r,w,maxfd;
653         time_t next_scantime = 0;
654         bool sosSend = false;
655
656         r = w = 0;
657         hasStarted();
658         eDebug("eStreamThreadWeb started");
659         while (!m_stop) {
660                 pthread_testcancel();
661                 FD_ZERO(&rfds);
662                 FD_ZERO(&wfds);
663                 maxfd = 0;
664                 timeout.tv_sec = 1;
665                 timeout.tv_usec = 0;
666                 if (r < bufsize) {
667                         FD_SET(m_srcfd, &rfds);
668                         maxfd = MAX(maxfd, m_srcfd);
669                 }
670                 if (w < r) {
671                         FD_SET(m_destfd, &wfds);
672                         maxfd = MAX(maxfd, m_destfd);
673                 }
674                 rc = select(maxfd+1, &rfds, &wfds, NULL, &timeout);
675                 if (rc == 0) {
676                         eDebug("eStreamThreadWeb::thread: timeout!");
677                         continue;
678                 }
679                 if (rc < 0) {
680                         eDebug("eStreamThreadWeb::thread: error in select (%d)", errno);
681                         break;
682                 }
683                 if (FD_ISSET(m_srcfd, &rfds)) {
684                         rc = ::read(m_srcfd, buf+r, bufsize - r);
685                         if (rc < 0) {
686                                 eDebug("eStreamThreadWeb::thread: error in read (%d)", errno);
687                                 m_messagepump.send(evtReadError);
688                                 break;
689                         } else if (rc == 0) {
690                                 eof = true;
691                         } else {
692                                 if (!sosSend) {
693                                         sosSend = true;
694                                         m_messagepump.send(evtSOS);
695                                 }
696                                 r += rc;
697                                 if (r == bufsize) eDebug("eStreamThreadWeb::thread: buffer full");
698                         }
699                 }
700                 if (FD_ISSET(m_destfd, &wfds) && (w < r) && ((r > bufsize/4) || eof)) {
701                         rc = ::write(m_destfd, buf+w, r-w);
702                         if (rc < 0) {
703                                 eDebug("eStreamThreadWeb::thread: error in write (%d)", errno);
704                                 m_messagepump.send(evtWriteError);
705                                 break;
706                         }
707                         w += rc;
708                         //eDebug("eStreamThreadWeb::thread: buffer r=%d w=%d",r,w);
709                         if (w == r) {
710                                 if (time(0) >= next_scantime) {
711                                         if (scanAudioInfo(buf, r)) {
712                                                 m_messagepump.send(evtStreamInfo);
713                                                 next_scantime = time(0) + 1;
714                                         }
715                                 }
716                                 w = r = 0;
717                         }
718                 }
719                 if (eof && (r==w)) {
720                         ::close(m_destfd);
721                         m_destfd = -1;
722                         ::close(m_srcfd);
723                         m_srcfd = -1;
724                         m_messagepump.send(evtEOS);
725                         break;
726                 }
727         }
728         eDebug("eStreamThreadWeb end");
729 }
730
731 void eStreamThreadWeb::thread_finished() {
732         if (m_srcfd >= 0) ::close(m_srcfd);
733         if (m_destfd >= 0) ::close(m_destfd);
734         eDebug("eStreamThreadWeb closed");
735 }
736
737 eAutoInitPtr<eServiceFactoryWebTS> init_eServiceFactoryWebTS(eAutoInitNumbers::service+1, "eServiceFactoryWebTS");
738
739 PyMODINIT_FUNC
740 initservicewebts(void)
741 {
742         Py_InitModule("servicewebts", NULL);
743 }