MythTV  master
httplivestream.cpp
Go to the documentation of this file.
1 /* -*- Mode: c++ -*-
2  *
3  * Class HTTPLiveStream
4  *
5  * Copyright (C) Chris Pinkham 2011
6  *
7  * This program is free software; you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation; either version 2 of the License, or
10  * (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program; if not, write to the Free Software
19  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
20  */
21 
22 // POSIX headers
23 #include <unistd.h> // for usleep
24 
25 // C headers
26 #include <cstdio>
27 
28 #include <QDir>
29 #include <QFile>
30 #include <QFileInfo>
31 #include <QIODevice>
32 #include <QRunnable>
33 #include <QUrl>
34 
35 #include "mythcorecontext.h"
36 #include "mythdate.h"
37 #include "mythdirs.h"
38 #include "mythtimer.h"
39 #include "mthreadpool.h"
40 #include "mythsystemlegacy.h"
41 #include "exitcodes.h"
42 #include "mythlogging.h"
43 #include "storagegroup.h"
44 #include "httplivestream.h"
45 
46 #define LOC QString("HLS(%1): ").arg(m_sourceFile)
47 #define LOC_ERR QString("HLS(%1) Error: ").arg(m_sourceFile)
48 #define SLOC QString("HLS(): ")
49 #define SLOC_ERR QString("HLS() Error: ")
50 
57 class HTTPLiveStreamThread : public QRunnable
58 {
59  public:
64  explicit HTTPLiveStreamThread(int streamid)
65  : m_streamID(streamid) {}
66 
72  void run(void) override // QRunnable
73  {
75 
76  QString command = GetAppBinDir() +
77  QString("mythtranscode --hls --hlsstreamid %1")
79 
80  uint result = myth_system(command, flags);
81 
82  if (result != GENERIC_EXIT_OK)
83  LOG(VB_GENERAL, LOG_WARNING, SLOC +
84  QString("Command '%1' returned %2")
85  .arg(command).arg(result));
86  }
87 
88  private:
90 };
91 
92 
93 HTTPLiveStream::HTTPLiveStream(QString srcFile, uint16_t width, uint16_t height,
94  uint32_t bitrate, uint32_t abitrate,
95  uint16_t maxSegments, uint16_t segmentSize,
96  uint32_t aobitrate, int32_t srate)
97  : m_writing(false),
98  m_streamid(-1), m_sourceFile(srcFile),
99  m_sourceWidth(0), m_sourceHeight(0),
100  m_segmentSize(segmentSize), m_maxSegments(maxSegments),
101  m_segmentCount(0), m_startSegment(0),
102  m_curSegment(0),
103  m_height(height), m_width(width),
104  m_bitrate(bitrate),
105  m_audioBitrate(abitrate), m_audioOnlyBitrate(aobitrate),
106  m_sampleRate(srate),
107  m_created(MythDate::current()),
108  m_lastModified(MythDate::current()),
109  m_percentComplete(0),
110  m_status(kHLSStatusUndefined)
111 {
112  if ((m_width == 0) && (m_height == 0))
113  m_width = 640;
114 
115  if (m_bitrate == 0)
116  m_bitrate = 800000;
117 
118  if (m_audioBitrate == 0)
119  m_audioBitrate = 64000;
120 
121  if (m_segmentSize == 0)
122  m_segmentSize = 4;
123 
124  if (m_audioOnlyBitrate == 0)
125  m_audioOnlyBitrate = 64000;
126 
128 
129  QFileInfo finfo(m_sourceFile);
130  m_outBase = finfo.fileName() +
131  QString(".%1x%2_%3kV_%4kA").arg(m_width).arg(m_height)
132  .arg(m_bitrate/1000).arg(m_audioBitrate/1000);
133 
134  SetOutputVars();
135 
136  m_fullURL = m_httpPrefix + m_outBase + ".m3u8";
138 
139  StorageGroup sgroup("Streaming", gCoreContext->GetHostName());
140  m_outDir = sgroup.GetFirstDir();
141  QDir outDir(m_outDir);
142 
143  if (!outDir.exists() && !outDir.mkdir(m_outDir))
144  {
145  LOG(VB_RECORD, LOG_ERR, "Unable to create HTTP Live Stream output "
146  "directory, Live Stream will not be created");
147  return;
148  }
149 
150  AddStream();
151 }
152 
154  : m_writing(false),
155  m_streamid(streamid)
156 {
157  LoadFromDB();
158 }
159 
161 {
162  if (m_writing)
163  {
164  WritePlaylist(false, true);
165  if (m_audioOnlyBitrate)
166  WritePlaylist(true, true);
167  }
168 }
169 
171 {
172  if ((m_streamid == -1) ||
173  (!WriteHTML()) ||
174  (!WriteMetaPlaylist()) ||
176  (!UpdateStatusMessage("Transcode Starting")))
177  return false;
178 
179  m_writing = true;
180 
181  return true;
182 }
183 
184 QString HTTPLiveStream::GetFilename(uint16_t segmentNumber, bool fileOnly,
185  bool audioOnly, bool encoded) const
186 {
187  QString filename;
188 
189  if (encoded)
190  filename = audioOnly ? m_audioOutFileEncoded : m_outFileEncoded;
191  else
192  filename = audioOnly ? m_audioOutFile : m_outFile;
193 
194  filename += ".%1.ts";
195 
196  if (!fileOnly)
197  filename = m_outDir + "/" + filename;
198 
199  if (segmentNumber)
200  return filename.arg(segmentNumber, 6, 10, QChar('0'));
201 
202  return filename.arg(1, 6, 10, QChar('0'));
203 }
204 
205 QString HTTPLiveStream::GetCurrentFilename(bool audioOnly, bool encoded) const
206 {
207  return GetFilename(m_curSegment, false, audioOnly, encoded);
208 }
209 
211 {
213 
214  QString tmpBase = QString("");
215  QString tmpFullURL = QString("");
216  QString tmpRelURL = QString("");
217 
218  if (m_width && m_height)
219  {
220  tmpBase = m_outBase;
221  tmpFullURL = m_fullURL;
222  tmpRelURL = m_relativeURL;
223  }
224 
225  // Check that this stream has not already been created.
226  // We want to avoid creating multiple identical streams and transcode
227  // jobs
228  MSqlQuery query(MSqlQuery::InitCon());
229  query.prepare(
230  "SELECT id FROM livestream "
231  "WHERE "
232  "(width = :WIDTH OR height = :HEIGHT) AND bitrate = :BITRATE AND "
233  "audioonlybitrate = :AUDIOONLYBITRATE AND samplerate = :SAMPLERATE AND "
234  "audiobitrate = :AUDIOBITRATE AND segmentsize = :SEGMENTSIZE AND "
235  "sourcefile = :SOURCEFILE AND status <= :STATUS ");
236  query.bindValue(":WIDTH", m_width);
237  query.bindValue(":HEIGHT", m_height);
238  query.bindValue(":BITRATE", m_bitrate);
239  query.bindValue(":AUDIOBITRATE", m_audioBitrate);
240  query.bindValue(":SEGMENTSIZE", m_segmentSize);
241  query.bindValue(":STATUS", (int)kHLSStatusCompleted);
242  query.bindValue(":SOURCEFILE", m_sourceFile);
243  query.bindValue(":AUDIOONLYBITRATE", m_audioOnlyBitrate);
244  query.bindValue(":SAMPLERATE", (m_sampleRate == -1) ? 0 : m_sampleRate); // samplerate column is unsigned, -1 becomes 0
245 
246  if (!query.exec())
247  {
248  LOG(VB_GENERAL, LOG_ERR, LOC + "LiveStream existing stream check failed.");
249  return -1;
250  }
251 
252  if (!query.next())
253  {
254  query.prepare(
255  "INSERT INTO livestream "
256  " ( width, height, bitrate, audiobitrate, segmentsize, "
257  " maxsegments, startsegment, currentsegment, segmentcount, "
258  " percentcomplete, created, lastmodified, relativeurl, "
259  " fullurl, status, statusmessage, sourcefile, sourcehost, "
260  " sourcewidth, sourceheight, outdir, outbase, "
261  " audioonlybitrate, samplerate ) "
262  "VALUES "
263  " ( :WIDTH, :HEIGHT, :BITRATE, :AUDIOBITRATE, :SEGMENTSIZE, "
264  " :MAXSEGMENTS, 0, 0, 0, "
265  " 0, :CREATED, :LASTMODIFIED, :RELATIVEURL, "
266  " :FULLURL, :STATUS, :STATUSMESSAGE, :SOURCEFILE, :SOURCEHOST, "
267  " :SOURCEWIDTH, :SOURCEHEIGHT, :OUTDIR, :OUTBASE, "
268  " :AUDIOONLYBITRATE, :SAMPLERATE ) ");
269  query.bindValue(":WIDTH", m_width);
270  query.bindValue(":HEIGHT", m_height);
271  query.bindValue(":BITRATE", m_bitrate);
272  query.bindValue(":AUDIOBITRATE", m_audioBitrate);
273  query.bindValue(":SEGMENTSIZE", m_segmentSize);
274  query.bindValue(":MAXSEGMENTS", m_maxSegments);
275  query.bindValue(":CREATED", m_created);
276  query.bindValue(":LASTMODIFIED", m_lastModified);
277  query.bindValue(":RELATIVEURL", tmpRelURL);
278  query.bindValue(":FULLURL", tmpFullURL);
279  query.bindValue(":STATUS", (int)m_status);
280  query.bindValue(":STATUSMESSAGE",
281  QString("Waiting for mythtranscode startup."));
282  query.bindValue(":SOURCEFILE", m_sourceFile);
283  query.bindValue(":SOURCEHOST", gCoreContext->GetHostName());
284  query.bindValue(":SOURCEWIDTH", 0);
285  query.bindValue(":SOURCEHEIGHT", 0);
286  query.bindValue(":OUTDIR", m_outDir);
287  query.bindValue(":OUTBASE", tmpBase);
288  query.bindValue(":AUDIOONLYBITRATE", m_audioOnlyBitrate);
289  query.bindValue(":SAMPLERATE", (m_sampleRate == -1) ? 0 : m_sampleRate); // samplerate column is unsigned, -1 becomes 0
290 
291  if (!query.exec())
292  {
293  LOG(VB_GENERAL, LOG_ERR, LOC + "LiveStream insert failed.");
294  return -1;
295  }
296 
297  if (!query.exec("SELECT LAST_INSERT_ID()") || !query.next())
298  {
299  LOG(VB_GENERAL, LOG_ERR, LOC + "Unable to query LiveStream streamid.");
300  return -1;
301  }
302  }
303 
304  m_streamid = query.value(0).toUInt();
305 
306  return m_streamid;
307 }
308 
310 {
311  if (m_streamid == -1)
312  return false;
313 
314  MSqlQuery query(MSqlQuery::InitCon());
315 
316  ++m_curSegment;
317  ++m_segmentCount;
318 
319  if (!m_startSegment)
321 
322  if ((m_maxSegments) &&
324  {
325  QString thisFile = GetFilename(m_startSegment);
326 
327  if (!QFile::remove(thisFile))
328  LOG(VB_GENERAL, LOG_ERR, LOC +
329  QString("Unable to delete %1.").arg(thisFile));
330 
331  ++m_startSegment;
332  --m_segmentCount;
333  }
334 
335  SaveSegmentInfo();
336  WritePlaylist(false);
337 
338  if (m_audioOnlyBitrate)
339  WritePlaylist(true);
340 
341  return true;
342 }
343 
345 {
346  if (m_streamid == -1)
347  return QString();
348 
349  QString outFile = m_outDir + "/" + m_outBase + ".html";
350  return outFile;
351 }
352 
354 {
355  if (m_streamid == -1)
356  return false;
357 
358  QString outFile = m_outDir + "/" + m_outBase + ".html";
359  QFile file(outFile);
360 
361  if (!file.open(QIODevice::WriteOnly))
362  {
363  LOG(VB_RECORD, LOG_ERR, QString("Error opening %1").arg(outFile));
364  return false;
365  }
366 
367  file.write(QString(
368  "<html>\n"
369  " <head>\n"
370  " <title>%1</title>\n"
371  " </head>\n"
372  " <body style='background-color:#FFFFFF;'>\n"
373  " <center>\n"
374  " <video controls>\n"
375  " <source src='%2.m3u8' />\n"
376  " </video>\n"
377  " </center>\n"
378  " </body>\n"
379  "</html>\n"
380  ).arg(m_sourceFile).arg(m_outBaseEncoded)
381  .toLatin1());
382 
383  file.close();
384 
385  return true;
386 }
387 
389 {
390  if (m_streamid == -1)
391  return QString();
392 
393  QString outFile = m_outDir + "/" + m_outBase + ".m3u8";
394  return outFile;
395 }
396 
398 {
399  if (m_streamid == -1)
400  return false;
401 
402  QString outFile = GetMetaPlaylistName();
403  QFile file(outFile);
404 
405  if (!file.open(QIODevice::WriteOnly))
406  {
407  LOG(VB_RECORD, LOG_ERR, QString("Error opening %1").arg(outFile));
408  return false;
409  }
410 
411  file.write(QString(
412  "#EXTM3U\n"
413  "#EXT-X-VERSION:4\n"
414  "#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"AV\",NAME=\"Main\",DEFAULT=YES,URI=\"%2.m3u8\"\n"
415  "#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=%1\n"
416  "%2.m3u8\n"
417  ).arg((int)((m_bitrate + m_audioBitrate) * 1.1))
418  .arg(m_outFileEncoded).toLatin1());
419 
420  if (m_audioOnlyBitrate)
421  {
422  file.write(QString(
423  "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"AO\",NAME=\"Main\",DEFAULT=NO,URI=\"%2.m3u8\"\n"
424  "#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=%1\n"
425  "%2.m3u8\n"
426  ).arg((int)((m_audioOnlyBitrate) * 1.1))
427  .arg(m_audioOutFileEncoded).toLatin1());
428  }
429 
430  file.close();
431 
432  return true;
433 }
434 
435 QString HTTPLiveStream::GetPlaylistName(bool audioOnly) const
436 {
437  if (m_streamid == -1)
438  return QString();
439 
440  if (audioOnly && m_audioOutFile.isEmpty())
441  return QString();
442 
443  QString base = audioOnly ? m_audioOutFile : m_outFile;
444  QString outFile = m_outDir + "/" + base + ".m3u8";
445  return outFile;
446 }
447 
448 bool HTTPLiveStream::WritePlaylist(bool audioOnly, bool writeEndTag)
449 {
450  if (m_streamid == -1)
451  return false;
452 
453  QString outFile = GetPlaylistName(audioOnly);
454  QString tmpFile = outFile + ".tmp";
455 
456  QFile file(tmpFile);
457 
458  if (!file.open(QIODevice::WriteOnly))
459  {
460  LOG(VB_RECORD, LOG_ERR, QString("Error opening %1").arg(tmpFile));
461  return false;
462  }
463 
464  file.write(QString(
465  "#EXTM3U\n"
466  "#EXT-X-ALLOW-CACHE:YES\n"
467  "#EXT-X-TARGETDURATION:%1\n"
468  "#EXT-X-MEDIA-SEQUENCE:%2\n"
469  ).arg(m_segmentSize).arg(m_startSegment).toLatin1());
470 
471  if (writeEndTag)
472  file.write("#EXT-X-ENDLIST\n");
473 
474  // Don't write out the current segment until the end
475  unsigned int tmpSegCount = m_segmentCount - 1;
476  unsigned int i = 0;
477  unsigned int segmentid = m_startSegment;
478 
479  if (writeEndTag)
480  ++tmpSegCount;
481 
482  while (i < tmpSegCount)
483  {
484  file.write(QString(
485  "#EXTINF:%1,\n"
486  "%2\n"
487  ).arg(m_segmentSize)
488  .arg(GetFilename(segmentid + i, true, audioOnly, true)).toLatin1());
489 
490  ++i;
491  }
492 
493  file.close();
494 
495  if(rename(tmpFile.toLatin1().constData(),
496  outFile.toLatin1().constData()) == -1)
497  {
498  LOG(VB_RECORD, LOG_ERR, LOC +
499  QString("Error renaming %1 to %2").arg(tmpFile).arg(outFile) + ENO);
500  return false;
501  }
502 
503  return true;
504 }
505 
507 {
508  if (m_streamid == -1)
509  return false;
510 
511  MSqlQuery query(MSqlQuery::InitCon());
512  query.prepare(
513  "UPDATE livestream "
514  "SET startsegment = :START, currentsegment = :CURRENT, "
515  " segmentcount = :COUNT "
516  "WHERE id = :STREAMID; ");
517  query.bindValue(":START", m_startSegment);
518  query.bindValue(":CURRENT", m_curSegment);
519  query.bindValue(":COUNT", m_segmentCount);
520  query.bindValue(":STREAMID", m_streamid);
521 
522  if (query.exec())
523  return true;
524 
525  LOG(VB_GENERAL, LOG_ERR, LOC +
526  QString("Unable to update segment info for streamid %1")
527  .arg(m_streamid));
528  return false;
529 }
530 
532  uint16_t srcwidth, uint16_t srcheight)
533 {
534  if (m_streamid == -1)
535  return false;
536 
537  QFileInfo finfo(m_sourceFile);
538  QString newOutBase = finfo.fileName() +
539  QString(".%1x%2_%3kV_%4kA").arg(width).arg(height)
540  .arg(m_bitrate/1000).arg(m_audioBitrate/1000);
541  QString newFullURL = m_httpPrefix + newOutBase + ".m3u8";
542  QString newRelativeURL = m_httpPrefixRel + newOutBase + ".m3u8";
543 
544  MSqlQuery query(MSqlQuery::InitCon());
545  query.prepare(
546  "UPDATE livestream "
547  "SET width = :WIDTH, height = :HEIGHT, "
548  " sourcewidth = :SRCWIDTH, sourceheight = :SRCHEIGHT, "
549  " fullurl = :FULLURL, relativeurl = :RELATIVEURL, "
550  " outbase = :OUTBASE "
551  "WHERE id = :STREAMID; ");
552  query.bindValue(":WIDTH", width);
553  query.bindValue(":HEIGHT", height);
554  query.bindValue(":SRCWIDTH", srcwidth);
555  query.bindValue(":SRCHEIGHT", srcheight);
556  query.bindValue(":FULLURL", newFullURL);
557  query.bindValue(":RELATIVEURL", newRelativeURL);
558  query.bindValue(":OUTBASE", newOutBase);
559  query.bindValue(":STREAMID", m_streamid);
560 
561  if (!query.exec())
562  {
563  LOG(VB_GENERAL, LOG_ERR, LOC +
564  QString("Unable to update segment info for streamid %1")
565  .arg(m_streamid));
566  return false;
567  }
568 
569  m_width = width;
570  m_height = height;
571  m_sourceWidth = srcwidth;
572  m_sourceHeight = srcheight;
573  m_outBase = newOutBase;
574  m_fullURL = newFullURL;
575  m_relativeURL = newRelativeURL;
576 
577  SetOutputVars();
578 
579  return true;
580 }
581 
583 {
584  if (m_streamid == -1)
585  return false;
586 
587  if ((m_status == kHLSStatusStopping) &&
588  (status == kHLSStatusRunning))
589  {
590  LOG(VB_RECORD, LOG_DEBUG, LOC +
591  QString("Attempted to switch streamid %1 from "
592  "Stopping to Running State").arg(m_streamid));
593  return false;
594  }
595 
596  QString mStatusStr = StatusToString(m_status);
597  QString statusStr = StatusToString(status);
598  LOG(VB_RECORD, LOG_DEBUG, LOC +
599  QString("Switch streamid %1 from %2 to %3")
600  .arg(m_streamid).arg(mStatusStr).arg(statusStr));
601 
602  m_status = status;
603 
604  MSqlQuery query(MSqlQuery::InitCon());
605  query.prepare(
606  "UPDATE livestream "
607  "SET status = :STATUS "
608  "WHERE id = :STREAMID; ");
609  query.bindValue(":STATUS", (int)status);
610  query.bindValue(":STREAMID", m_streamid);
611 
612  if (query.exec())
613  return true;
614 
615  LOG(VB_GENERAL, LOG_ERR, LOC +
616  QString("Unable to update status for streamid %1").arg(m_streamid));
617  return false;
618 }
619 
621 {
622  if (m_streamid == -1)
623  return false;
624 
625  MSqlQuery query(MSqlQuery::InitCon());
626  query.prepare(
627  "UPDATE livestream "
628  "SET statusmessage = :MESSAGE "
629  "WHERE id = :STREAMID; ");
630  query.bindValue(":MESSAGE", message);
631  query.bindValue(":STREAMID", m_streamid);
632 
633  if (query.exec())
634  {
635  m_statusMessage = message;
636  return true;
637  }
638 
639  LOG(VB_GENERAL, LOG_ERR, LOC +
640  QString("Unable to update status message for streamid %1")
641  .arg(m_streamid));
642  return false;
643 }
644 
646 {
647  if (m_streamid == -1)
648  return false;
649 
650  MSqlQuery query(MSqlQuery::InitCon());
651  query.prepare(
652  "UPDATE livestream "
653  "SET percentcomplete = :PERCENT "
654  "WHERE id = :STREAMID; ");
655  query.bindValue(":PERCENT", percent);
656  query.bindValue(":STREAMID", m_streamid);
657 
658  if (query.exec())
659  {
660  m_percentComplete = percent;
661  return true;
662  }
663 
664  LOG(VB_GENERAL, LOG_ERR, LOC +
665  QString("Unable to update percent complete for streamid %1")
666  .arg(m_streamid));
667  return false;
668 }
669 
671 {
672  switch (status) {
673  case kHLSStatusUndefined : return QString("Undefined");
674  case kHLSStatusQueued : return QString("Queued");
675  case kHLSStatusStarting : return QString("Starting");
676  case kHLSStatusRunning : return QString("Running");
677  case kHLSStatusCompleted : return QString("Completed");
678  case kHLSStatusErrored : return QString("Errored");
679  case kHLSStatusStopping : return QString("Stopping");
680  case kHLSStatusStopped : return QString("Stopped");
681  };
682 
683  return QString("Unknown status value");
684 }
685 
687 {
688  if (m_streamid == -1)
689  return false;
690 
691  MSqlQuery query(MSqlQuery::InitCon());
692  query.prepare(
693  "SELECT width, height, bitrate, audiobitrate, segmentsize, "
694  " maxsegments, startsegment, currentsegment, segmentcount, "
695  " percentcomplete, created, lastmodified, relativeurl, "
696  " fullurl, status, statusmessage, sourcefile, sourcehost, "
697  " sourcewidth, sourceheight, outdir, outbase, audioonlybitrate, "
698  " samplerate "
699  "FROM livestream "
700  "WHERE id = :STREAMID; ");
701  query.bindValue(":STREAMID", m_streamid);
702 
703  if (!query.exec() || !query.next())
704  {
705  LOG(VB_GENERAL, LOG_ERR, LOC +
706  QString("Unable to query DB info for stream %1")
707  .arg(m_streamid));
708  return false;
709  }
710 
711  m_width = query.value(0).toUInt();
712  m_height = query.value(1).toUInt();
713  m_bitrate = query.value(2).toUInt();
714  m_audioBitrate = query.value(3).toUInt();
715  m_segmentSize = query.value(4).toUInt();
716  m_maxSegments = query.value(5).toUInt();
717  m_startSegment = query.value(6).toUInt();
718  m_curSegment = query.value(7).toUInt();
719  m_segmentCount = query.value(8).toUInt();
720  m_percentComplete = query.value(9).toUInt();
721  m_created = MythDate::as_utc(query.value(10).toDateTime());
722  m_lastModified = MythDate::as_utc(query.value(11).toDateTime());
723  m_relativeURL = query.value(12).toString();
724  m_fullURL = query.value(13).toString();
725  m_status = (HTTPLiveStreamStatus)(query.value(14).toInt());
726  m_statusMessage = query.value(15).toString();
727  m_sourceFile = query.value(16).toString();
728  m_sourceHost = query.value(17).toString();
729  m_sourceWidth = query.value(18).toUInt();
730  m_sourceHeight = query.value(19).toUInt();
731  m_outDir = query.value(20).toString();
732  m_outBase = query.value(21).toString();
733  m_audioOnlyBitrate = query.value(22).toUInt();
734  m_sampleRate = query.value(23).toUInt();
735 
736  SetOutputVars();
737 
738  return true;
739 }
740 
742 {
743  m_outBaseEncoded = QString(QUrl::toPercentEncoding(m_outBase, "", " "));
744 
745  m_outFile = m_outBase + ".av";
747 
748  if (m_audioOnlyBitrate)
749  {
751  QString(".ao_%1kA").arg(m_audioOnlyBitrate/1000);
753  QString(".ao_%1kA").arg(m_audioOnlyBitrate/1000);
754  }
755 
756  m_httpPrefix = gCoreContext->GetSetting("HTTPLiveStreamPrefix", QString(
757  "http://%1:%2/StorageGroup/Streaming/")
760 
761  if (!m_httpPrefix.endsWith("/"))
762  m_httpPrefix.append("/");
763 
764  if (!gCoreContext->GetSetting("HTTPLiveStreamPrefixRel").isEmpty())
765  {
766  m_httpPrefixRel = gCoreContext->GetSetting("HTTPLiveStreamPrefixRel");
767  if (!m_httpPrefix.endsWith("/"))
768  m_httpPrefix.append("/");
769  }
770  else if (m_httpPrefix.contains("/StorageGroup/Streaming/"))
771  m_httpPrefixRel = "/StorageGroup/Streaming/";
772  else
773  m_httpPrefixRel = "";
774 }
775 
777 {
778  if (m_streamid == -1)
779  return kHLSStatusUndefined;
780 
781  MSqlQuery query(MSqlQuery::InitCon());
782  query.prepare(
783  "SELECT status FROM livestream "
784  "WHERE id = :STREAMID; ");
785  query.bindValue(":STREAMID", m_streamid);
786 
787  if (!query.exec() || !query.next())
788  {
789  LOG(VB_GENERAL, LOG_ERR, LOC +
790  QString("Unable to check stop status for stream %1")
791  .arg(m_streamid));
792  return kHLSStatusUndefined;
793  }
794 
795  return (HTTPLiveStreamStatus)query.value(0).toInt();
796 }
797 
799 {
800  if (m_streamid == -1)
801  return false;
802 
803  MSqlQuery query(MSqlQuery::InitCon());
804  query.prepare(
805  "SELECT status FROM livestream "
806  "WHERE id = :STREAMID; ");
807  query.bindValue(":STREAMID", m_streamid);
808 
809  if (!query.exec() || !query.next())
810  {
811  LOG(VB_GENERAL, LOG_ERR, LOC +
812  QString("Unable to check stop status for stream %1")
813  .arg(m_streamid));
814  return false;
815  }
816 
817  if (query.value(0).toInt() == (int)kHLSStatusStopping)
818  return true;
819 
820  return false;
821 }
822 
824 {
825  if (GetDBStatus() != kHLSStatusQueued)
826  return GetLiveStreamInfo();
827 
828  HTTPLiveStreamThread *streamThread =
831  "HTTPLiveStream");
832  MythTimer statusTimer;
833  int delay = 250000;
834  statusTimer.start();
835 
837  while ((status == kHLSStatusQueued) &&
838  ((statusTimer.elapsed() / 1000) < 30))
839  {
840  delay = (int)(delay * 1.5);
841  usleep(delay);
842 
843  status = GetDBStatus();
844  }
845 
846  return GetLiveStreamInfo();
847 }
848 
850 {
851  MSqlQuery query(MSqlQuery::InitCon());
852  query.prepare(
853  "SELECT startSegment, segmentCount "
854  "FROM livestream "
855  "WHERE id = :STREAMID; ");
856  query.bindValue(":STREAMID", id);
857 
858  if (!query.exec() || !query.next())
859  {
860  LOG(VB_RECORD, LOG_ERR, "Error selecting stream info in RemoveStream");
861  return false;
862  }
863 
864  HTTPLiveStream *hls = new HTTPLiveStream(id);
865 
866  if (hls->GetDBStatus() == kHLSStatusRunning) {
868  }
869 
870  QString thisFile;
871  int startSegment = query.value(0).toInt();
872  int segmentCount = query.value(1).toInt();
873 
874  for (int x = 0; x < segmentCount; ++x)
875  {
876  thisFile = hls->GetFilename(startSegment + x);
877 
878  if (!thisFile.isEmpty() && !QFile::remove(thisFile))
879  LOG(VB_GENERAL, LOG_ERR, SLOC +
880  QString("Unable to delete %1.").arg(thisFile));
881 
882  thisFile = hls->GetFilename(startSegment + x, false, true);
883 
884  if (!thisFile.isEmpty() && !QFile::remove(thisFile))
885  LOG(VB_GENERAL, LOG_ERR, SLOC +
886  QString("Unable to delete %1.").arg(thisFile));
887  }
888 
889  thisFile = hls->GetMetaPlaylistName();
890  if (!thisFile.isEmpty() && !QFile::remove(thisFile))
891  LOG(VB_GENERAL, LOG_ERR, SLOC +
892  QString("Unable to delete %1.").arg(thisFile));
893 
894  thisFile = hls->GetPlaylistName();
895  if (!thisFile.isEmpty() && !QFile::remove(thisFile))
896  LOG(VB_GENERAL, LOG_ERR, SLOC +
897  QString("Unable to delete %1.").arg(thisFile));
898 
899  thisFile = hls->GetPlaylistName(true);
900  if (!thisFile.isEmpty() && !QFile::remove(thisFile))
901  LOG(VB_GENERAL, LOG_ERR, SLOC +
902  QString("Unable to delete %1.").arg(thisFile));
903 
904  thisFile = hls->GetHTMLPageName();
905  if (!thisFile.isEmpty() && !QFile::remove(thisFile))
906  LOG(VB_GENERAL, LOG_ERR, SLOC +
907  QString("Unable to delete %1.").arg(thisFile));
908 
909  query.prepare(
910  "DELETE FROM livestream "
911  "WHERE id = :STREAMID; ");
912  query.bindValue(":STREAMID", id);
913 
914  if (!query.exec())
915  LOG(VB_RECORD, LOG_ERR, "Error deleting stream info in RemoveStream");
916 
917  delete hls;
918  return true;
919 }
920 
922 {
923  MSqlQuery query(MSqlQuery::InitCon());
924  query.prepare(
925  "UPDATE livestream "
926  "SET status = :STATUS "
927  "WHERE id = :STREAMID; ");
928  query.bindValue(":STATUS", (int)kHLSStatusStopping);
929  query.bindValue(":STREAMID", id);
930 
931  if (!query.exec())
932  {
933  LOG(VB_GENERAL, LOG_ERR, SLOC +
934  QString("Unable to remove mark stream stopped for stream %1.")
935  .arg(id));
936  return nullptr;
937  }
938 
939  HTTPLiveStream *hls = new HTTPLiveStream(id);
940  if (!hls)
941  return nullptr;
942 
943  MythTimer statusTimer;
944  int delay = 250000;
945  statusTimer.start();
946 
947  HTTPLiveStreamStatus status = hls->GetDBStatus();
948  while ((status != kHLSStatusStopped) &&
949  (status != kHLSStatusCompleted) &&
950  (status != kHLSStatusErrored) &&
951  ((statusTimer.elapsed() / 1000) < 30))
952  {
953  delay = (int)(delay * 1.5);
954  usleep(delay);
955 
956  status = hls->GetDBStatus();
957  }
958 
959  hls->LoadFromDB();
960  DTC::LiveStreamInfo *pLiveStreamInfo = hls->GetLiveStreamInfo();
961 
962  delete hls;
963  return pLiveStreamInfo;
964 }
965 
967 // Content Service API helpers
969 
971  DTC::LiveStreamInfo *info)
972 {
973  if (!info)
974  info = new DTC::LiveStreamInfo();
975 
976  info->setId(m_streamid);
977  info->setWidth((int)m_width);
978  info->setHeight((int)m_height);
979  info->setBitrate((int)m_bitrate);
980  info->setAudioBitrate((int)m_audioBitrate);
981  info->setSegmentSize((int)m_segmentSize);
982  info->setMaxSegments((int)m_maxSegments);
983  info->setStartSegment((int)m_startSegment);
984  info->setCurrentSegment((int)m_curSegment);
985  info->setSegmentCount((int)m_segmentCount);
986  info->setPercentComplete((int)m_percentComplete);
987  info->setCreated(m_created);
988  info->setLastModified(m_lastModified);
989  info->setStatusStr(StatusToString(m_status));
990  info->setStatusInt((int)m_status);
991  info->setStatusMessage(m_statusMessage);
992  info->setSourceFile(m_sourceFile);
993  info->setSourceHost(m_sourceHost);
994  info->setAudioOnlyBitrate((int)m_audioOnlyBitrate);
995 
996  if (m_width && m_height) {
997  info->setRelativeURL(m_relativeURL);
998  info->setFullURL(m_fullURL);
999  info->setSourceWidth(m_sourceWidth);
1000  info->setSourceHeight(m_sourceHeight);
1001  }
1002 
1003  return info;
1004 }
1005 
1007 {
1009 
1010  QString sql = "SELECT id FROM livestream ";
1011 
1012  if (!FileName.isEmpty())
1013  sql += "WHERE sourcefile LIKE :FILENAME ";
1014 
1015  sql += "ORDER BY lastmodified DESC;";
1016 
1017  MSqlQuery query(MSqlQuery::InitCon());
1018  query.prepare(sql);
1019  if (!FileName.isEmpty())
1020  query.bindValue(":FILENAME", QString("%%1%").arg(FileName));
1021 
1022  if (!query.exec())
1023  {
1024  LOG(VB_GENERAL, LOG_ERR, SLOC + "Unable to get list of Live Streams");
1025  return infoList;
1026  }
1027 
1028  DTC::LiveStreamInfo *info = nullptr;
1029  HTTPLiveStream *hls = nullptr;
1030  while (query.next())
1031  {
1032  hls = new HTTPLiveStream(query.value(0).toUInt());
1033  info = infoList->AddNewLiveStreamInfo();
1034  hls->GetLiveStreamInfo(info);
1035  delete hls;
1036  }
1037 
1038  return infoList;
1039 }
1040 
1041 /* vim: set expandtab tabstop=4 shiftwidth=4: */
#define LOC
bool next(void)
Wrap QSqlQuery::next() so we can display the query results.
Definition: mythdbcon.cpp:794
bool SaveSegmentInfo(void)
void bindValue(const QString &placeholder, const QVariant &val)
Definition: mythdbcon.cpp:875
QString m_outFileEncoded
A QElapsedTimer based timer to replace use of QTime as a timer.
Definition: mythtimer.h:13
bool LoadFromDB(void)
HTTPLiveStreamStatus GetDBStatus(void) const
void startReserved(QRunnable *runnable, QString debugName, int waitForAvailMS=0)
#define GENERIC_EXIT_OK
Exited with no error.
Definition: exitcodes.h:10
uint32_t m_bitrate
QString m_httpPrefixRel
static DTC::LiveStreamInfoList * GetLiveStreamInfoList(const QString &FileName="")
uint16_t m_startSegment
QSqlQuery wrapper that fetches a DB connection from the connection pool.
Definition: mythdbcon.h:125
QString logPropagateArgs
Definition: logging.cpp:89
uint16_t m_curSegment
QString m_relativeURL
unsigned int uint
Definition: compat.h:140
MythCoreContext * gCoreContext
This global variable contains the MythCoreContext instance for the app.
uint16_t m_maxSegments
HTTPLiveStreamStatus
QDateTime as_utc(const QDateTime &old_dt)
Returns copy of QDateTime with TimeSpec set to UTC.
Definition: mythdate.cpp:23
bool WriteMetaPlaylist(void)
QString m_sourceHost
DTC::LiveStreamInfo * GetLiveStreamInfo(DTC::LiveStreamInfo *info=nullptr)
QString GetMasterServerIP(void)
Returns the Master Backend IP address If the address is an IPv6 address, the scope Id is removed.
void run(void) override
Runs mythtranscode for the given HTTP Live Stream ID.
QVariant value(int i) const
Definition: mythdbcon.h:182
QDateTime m_created
QString m_statusMessage
bool UpdateStatusMessage(QString message)
VERBOSE_PREAMBLE false
Definition: verbosedefs.h:85
HTTPLiveStreamStatus m_status
QString GetAppBinDir(void)
Definition: mythdirs.cpp:221
QDateTime current(bool stripped)
Returns current Date and Time in UTC.
Definition: mythdate.cpp:10
QString GetSetting(const QString &key, const QString &defaultval="")
uint16_t m_segmentSize
bool UpdatePercentComplete(int percent)
int GetStreamID(void) const
bool CheckStop(void)
QString GetPlaylistName(bool audioOnly=false) const
static bool RemoveStream(int id)
unsigned short uint16_t
Definition: iso6937tables.h:1
QString GetHTMLPageName(void) const
QString m_audioOutFileEncoded
QString GetCurrentFilename(bool audioOnly=false, bool encoded=false) const
static MSqlQueryInfo InitCon(ConnectionReuse=kNormalConnection)
Only use this in combination with MSqlQuery constructor.
Definition: mythdbcon.cpp:547
uint16_t m_sourceHeight
QDateTime m_lastModified
QString GetMetaPlaylistName(void) const
QString m_httpPrefix
uint16_t m_sourceWidth
static DTC::LiveStreamInfo * StopStream(int id)
uint32_t m_audioOnlyBitrate
uint myth_system(const QString &command, uint flags, uint timeout)
#define ENO
This can be appended to the LOG args with "+".
Definition: mythlogging.h:99
bool WritePlaylist(bool audioOnly=false, bool writeEndTag=false)
HTTPLiveStream(QString srcFile, uint16_t width=640, uint16_t height=480, uint32_t bitrate=800000, uint32_t abitrate=64000, uint16_t maxSegments=0, uint16_t segmentSize=10, uint32_t aobitrate=32000, int32_t srate=-1)
uint16_t m_percentComplete
void SetOutputVars(void)
bool UpdateSizeInfo(uint16_t width, uint16_t height, uint16_t srcwidth, uint16_t srcheight)
uint32_t m_audioBitrate
#define SLOC
bool InitForWrite(void)
int elapsed(void) const
Returns milliseconds elapsed since last start() or restart()
Definition: mythtimer.cpp:90
static MThreadPool * globalInstance(void)
DTC::LiveStreamInfo * StartStream(void)
uint16_t m_segmentCount
bool UpdateStatus(HTTPLiveStreamStatus status)
bool prepare(const QString &query)
QSqlQuery::prepare() is not thread safe in Qt <= 3.3.2.
Definition: mythdbcon.cpp:819
#define LOG(_MASK_, _LEVEL_, _STRING_)
Definition: mythlogging.h:41
bool AddSegment(void)
QString m_audioOutFile
HTTPLiveStreamThread(int streamid)
Constructor for creating a SystemEventThread.
LiveStreamInfo * AddNewLiveStreamInfo()
bool WriteHTML(void)
int GetMasterServerStatusPort(void)
Returns the Master Backend status port If no master server status port has been defined in the databa...
bool exec(void)
Wrap QSqlQuery::exec() so we can display SQL.
Definition: mythdbcon.cpp:615
QString m_sourceFile
QString GetFilename(uint16_t segmentNumber=0, bool fileOnly=false, bool audioOnly=false, bool encoded=false) const
QRunnable class for running mythtranscode for HTTP Live Streams.
int32_t m_sampleRate
QString GetHostName(void)
void start(void)
starts measuring elapsed time.
Definition: mythtimer.cpp:47
QString StatusToString(HTTPLiveStreamStatus status)
avoid blocking LIRC & Joystick Menu
Definition: mythsystem.h:34
QString m_outBaseEncoded