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