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