//Handler for ffmpeg timer to kill the process
        static void OnCaptureTimer(object obj)
        {
            bool killProcess = false;
            CaptureProcessInfo captureProcessInfo = obj as CaptureProcessInfo;

            //Are we done?
            if (DateTime.Now >= captureProcessInfo.timerDone)
            {
                killProcess = true;
                captureProcessInfo.logWriter.WriteLine($"{DateTime.Now}: Timer is up.  Killing capture process");
            }

            //Have we been canclled?
            if (captureProcessInfo.cancellationToken != null && captureProcessInfo.cancellationToken.IsCancellationRequested)
            {
                killProcess = true;
                captureProcessInfo.logWriter.WriteLine($"{DateTime.Now}: Task has been cancelled.  Killing capture process");
            }

            //Make sure file is still growing at a reasonable pace.  Otherwise, kill the process
            if (!killProcess)
            {
                //Grab file info
                FileInfo fileInfo = new FileInfo(captureProcessInfo.outputPath);

                //Make sure file even exists!
                if (!fileInfo.Exists)
                {
                    killProcess = true;
                    captureProcessInfo.logWriter.WriteLine($"{DateTime.Now}: ERROR: File {captureProcessInfo.outputPath} doesn't exist.  Feed is bad.");
                }
                else
                {
                    //Make sure file size (rate) is fine
                    long fileSize  = fileInfo.Length;
                    long kBytesSec = ((fileSize - captureProcessInfo.fileSize) / captureProcessInfo.interval) / 1000;
                    if (kBytesSec <= captureProcessInfo.acceptableRate)
                    {
                        killProcess = true;
                        captureProcessInfo.logWriter.WriteLine($"{DateTime.Now}: ERROR: File size no longer growing. (Current Rate: ({kBytesSec} KB/s)  Killing capture process.");
                    }
                    captureProcessInfo.fileSize     = fileSize;
                    captureProcessInfo.avgKBytesSec = (captureProcessInfo.avgKBytesSec + kBytesSec) / 2;
                }
            }

            //Kill process if needed
            Process p = captureProcessInfo.process;

            if (killProcess && p != null && !p.HasExited)
            {
                p.Kill();
                p.WaitForExit();
            }
        }
        public CaptureProcessInfo ExecProcess(TextWriter logWriter, string exe, string cmdLineArgs, int timeout, string outputPath, CancellationToken cancellationToken, bool bestChannelIsSelected)
        {
            //Create our process
            var processInfo = new ProcessStartInfo
            {
                FileName  = exe,
                Arguments = cmdLineArgs,
                RedirectStandardOutput = true,
                RedirectStandardError  = true
            };
            Process process = Process.Start(processInfo);

            //Let's build a timer to kill the process when done
            CaptureProcessInfo captureProcessInfo = null;
            Timer captureTimer = null;

            if (timeout > 0)
            {
                int  interval       = 10; //# of seconds between timer/file checks
                long acceptableRate = Convert.ToInt32(configuration["acceptableRate"]);

                //create capture process info
                DateTime timerDone = DateTime.Now.AddMinutes(timeout);
                captureProcessInfo = new CaptureProcessInfo(process, acceptableRate, interval, timerDone, outputPath, logWriter, cancellationToken, bestChannelIsSelected);

                //create timer
                TimeSpan intervalTime = new TimeSpan(0, 0, interval);
                logWriter.WriteLine($"{DateTime.Now}: Settting Timer for {timeout} minutes in the future to kill process.");
                captureTimer = new Timer(OnCaptureTimer, captureProcessInfo, intervalTime, intervalTime);
            }

            //Now, let's wait for the thing to exit
            logWriter.WriteLine(process.StandardError.ReadToEnd());
            logWriter.WriteLine(process.StandardOutput.ReadToEnd());
            process.WaitForExit();

            //Clean up timer
            if (timeout > 0 && captureTimer != null)
            {
                captureTimer.Dispose();
            }

            return(captureProcessInfo);
        }
        //Handler for ffmpeg timer to kill the process
        static void OnCaptureTimer(object obj)
        {
            bool killProcess = false;
            CaptureProcessInfo captureProcessInfo = obj as CaptureProcessInfo;

            //Are we done?
            if (DateTime.Now >= captureProcessInfo.timerDone)
            {
                killProcess = true;
                captureProcessInfo.logWriter.WriteLine($"{DateTime.Now}: Timer is up.  Killing capture process");
            }

            //Have we been canclled?
            if (captureProcessInfo.cancellationToken != null && captureProcessInfo.cancellationToken.IsCancellationRequested)
            {
                killProcess = true;
                captureProcessInfo.logWriter.WriteLine($"{DateTime.Now}: Task has been cancelled.  Killing capture process");
            }

            //Make sure file is still growing at a reasonable pace.  Otherwise, kill the process
            if (!killProcess)
            {
                //Grab file info
                FileInfo fileInfo = new FileInfo(captureProcessInfo.outputPath);

                //Make sure file even exists!
                if (!fileInfo.Exists)
                {
                    killProcess = true;
                    captureProcessInfo.logWriter.WriteLine($"{DateTime.Now}: ERROR: File {captureProcessInfo.outputPath} doesn't exist.  Feed is bad.");
                }
                else
                {
                    //Make sure file size (rate) is fine
                    long fileSize  = fileInfo.Length;
                    long kBytesSec = ((fileSize - captureProcessInfo.fileSize) / captureProcessInfo.interval) / 1000;
                    if ((kBytesSec <= captureProcessInfo.acceptableRate && !captureProcessInfo.bestChannelIsSelected) || kBytesSec < 5)  //If we haven't cycled through all the channels yet, see if we can do better.
                    {
                        killProcess = true;
                        captureProcessInfo.logWriter.WriteLine($"{DateTime.Now}: ERROR: File growing too slowly or not at all. (Current Rate: ({kBytesSec} KB/s)  Killing capture process.");
                    }

                    captureProcessInfo.fileSize = fileSize;

                    if (captureProcessInfo.avgKBytesSec == 0)
                    {
                        captureProcessInfo.avgKBytesSec = kBytesSec;
                    }
                    else
                    {
                        captureProcessInfo.avgKBytesSec = (captureProcessInfo.avgKBytesSec + kBytesSec) / 2;
                    }

                    //Log current KB rate every 30 minutes  (180 intervals)
                    if (captureProcessInfo.currentKbLogCount == 0 || captureProcessInfo.currentKbLogCount >= 180)
                    {
                        captureProcessInfo.logWriter.WriteLine($"{DateTime.Now}: Current Rate: {kBytesSec} KB/s  Avg Rate: {captureProcessInfo.avgKBytesSec}  File Size: {fileSize}");
                        captureProcessInfo.currentKbLogCount = 1;
                    }
                    captureProcessInfo.currentKbLogCount++;
                }
            }

            //Kill process if needed
            Process p = captureProcessInfo.process;

            if (killProcess && p != null && !p.HasExited)
            {
                p.Kill();
                p.WaitForExit();
            }
        }
Exemple #4
0
        //A key member function (along with BuildRecordSchedule) which does the bulk of the work
        //
        //I'll be a bit more verbose then usual so I can remember later what I was thinking
        private void CaptureStream(TextWriter logWriter, string hashValue, ChannelHistory channelHistory, RecordInfo recordInfo, VideoFileManager videoFileManager)
        {
            //Process manager for ffmpeg
            //
            //This is there ffmpeg is called, and a watchdog timer created to make sure things are going ok
            ProcessManager processManager = new ProcessManager(configuration);

            //Test internet connection and get a baseline
            //Right now, the baseline is not used for anything other than a number in the logs.
            //This could be taken out...
            long internetSpeed = TestInternet(logWriter);

            //Create servers object
            //This is the list of live247 servers we'll use to cycle through, finding the best quality stream
            Servers servers = new Servers(configuration["ServerList"]);

            //Create the server/channel selector object
            //
            //This object is what uses the heurstics to determine the right server/channel combo to use.
            //Factors like language, quality, and rate factor in.
            ServerChannelSelector scs = new ServerChannelSelector(logWriter, channelHistory, servers, recordInfo);

            //Marking time we started and when we should be done
            DateTime captureStarted   = DateTime.Now;
            DateTime captureTargetEnd = recordInfo.GetStartDT().AddMinutes(recordInfo.GetDuration());

            if (!string.IsNullOrEmpty(configuration["debug"]))
            {
                captureTargetEnd = DateTime.Now.AddMinutes(1);
            }
            DateTime lastStartedTime = captureStarted;
            TimeSpan duration        = (captureTargetEnd.AddMinutes(1)) - captureStarted; //the 1 minute takes care of alignment slop

            //Update capture history
            //This saves to channelhistory.json file and is used to help determine the initial order of server/channel combos
            channelHistory.GetChannelHistoryInfo(scs.GetChannelNumber()).recordingsAttempted += 1;
            channelHistory.GetChannelHistoryInfo(scs.GetChannelNumber()).lastAttempt          = DateTime.Now;

            //Build output file - the resulting capture file.
            //See VideoFileManager for specifics around file management (e.g. there can be multiple if errors are encountered etc)
            VideoFileInfo videoFileInfo = videoFileManager.AddCaptureFile(configuration["outputPath"]);

            //Email that show started
            if (recordInfo.emailFlag)
            {
                new Mailer().SendShowStartedMail(configuration, recordInfo);
            }

            //Build ffmpeg capture command line with first channel and get things rolling
            string cmdLineArgs = BuildCaptureCmdLineArgs(scs.GetServerName(), scs.GetChannelNumber(), hashValue, videoFileInfo.GetFullFile());

            logWriter.WriteLine($"=========================================");
            logWriter.WriteLine($"{DateTime.Now}: Starting {captureStarted} on server/channel {scs.GetServerName()}/{scs.GetChannelNumber()}.  Expect to be done by {captureTargetEnd}.");
            logWriter.WriteLine($"                      {configuration["ffmpegPath"]} {cmdLineArgs}");
            CaptureProcessInfo captureProcessInfo = processManager.ExecProcess(logWriter, configuration["ffmpegPath"], cmdLineArgs, (int)duration.TotalMinutes, videoFileInfo.GetFullFile(), recordInfo.cancellationToken, scs.IsBestSelected());

            logWriter.WriteLine($"{DateTime.Now}: Exited Capture.  Exit Code: {captureProcessInfo.process.ExitCode}");

            //Main loop to capture
            //
            //This loop is never entered if the first capture section completes without incident.  However, it is not uncommon
            //for errors to occur, and this loop takes care of determining the right next server/channel combo
            //as well as making sure that we don't just try forever.
            int numRetries = Convert.ToInt32(configuration["numberOfRetries"]);
            int retryNum   = 0;

            for (retryNum = 0; DateTime.Now <= captureTargetEnd && retryNum < numRetries && !recordInfo.cancelledFlag; retryNum++)
            {
                logWriter.WriteLine($"{DateTime.Now}: Capture Failed for server/channel {scs.GetServerName()}/{scs.GetChannelNumber()}. Retry {retryNum+1} of {configuration["numberOfRetries"]}");

                //Let's make sure the interwebs are still there.  If not, let's loop until they come back or the show ends.
                while (!IsInternetOk() && DateTime.Now <= captureTargetEnd)
                {
                    bool logFlag = false;
                    if (!logFlag)
                    {
                        logWriter.WriteLine($"{DateTime.Now}: Interwebs are down.  Checking every minute until back or show ends");
                    }
                    TimeSpan oneMinute = new TimeSpan(0, 1, 0);
                    Thread.Sleep(oneMinute);
                }

                //Check to see if we need to re-authenticate  (most tokens have a lifespan)
                int authMinutes = Convert.ToInt16(configuration["authMinutes"]);
                if (DateTime.Now > captureStarted.AddMinutes(authMinutes))
                {
                    logWriter.WriteLine($"{DateTime.Now}: It's been more than {authMinutes} authMinutes.  Time to re-authenticate");
                    Task <string> authTask = Authenticate();
                    hashValue = authTask.Result;
                    if (string.IsNullOrEmpty(hashValue))
                    {
                        Console.WriteLine($"{DateTime.Now}: ERROR: Unable to authenticate.  Check username and password?");
                        throw new Exception("Unable to authenticate during a retry");
                    }
                }

                //Log avg streaming rate for channel history
                logWriter.WriteLine($"{DateTime.Now}: Avg rate is {captureProcessInfo.avgKBytesSec}KB/s for {scs.GetServerName()}/{scs.GetChannelNumber()}");

                //Set new avg streaming rate for channel history
                channelHistory.SetServerAvgKBytesSec(scs.GetChannelNumber(), scs.GetServerName(), captureProcessInfo.avgKBytesSec);

                //Go to next channel if channel has been alive for less than 15 minutes
                //The idea is that if a server/channel has been stable for at least 15 minutes, no sense trying to find another.  (could make it worse)
                TimeSpan fifteenMin = new TimeSpan(0, 15, 0);
                if ((DateTime.Now - lastStartedTime) < fifteenMin)
                {
                    //Set rate for current server/channel pair
                    scs.SetAvgKBytesSec(captureProcessInfo.avgKBytesSec);

                    //Get correct server and channel (determined by heuristics)
                    if (!scs.IsBestSelected())
                    {
                        scs.GetNextServerChannel();
                        retryNum = -1; //reset retries since we haven't got through the server/channel list yet
                    }
                }
                else
                {
                    retryNum = -1; //reset retries since it's been more than 15 minutes
                }

                //Set new started time and calc new timer
                TimeSpan timeJustRecorded = DateTime.Now - lastStartedTime;
                lastStartedTime = DateTime.Now;
                TimeSpan timeLeft = captureTargetEnd - DateTime.Now;

                //Update channel history
                channelHistory.GetChannelHistoryInfo(scs.GetChannelNumber()).hoursRecorded       += timeJustRecorded.TotalHours;
                channelHistory.GetChannelHistoryInfo(scs.GetChannelNumber()).recordingsAttempted += 1;
                channelHistory.GetChannelHistoryInfo(scs.GetChannelNumber()).lastAttempt          = DateTime.Now;

                //Build output file
                videoFileInfo = videoFileManager.AddCaptureFile(configuration["outputPath"]);

                //Now get capture setup and going again
                cmdLineArgs = BuildCaptureCmdLineArgs(scs.GetServerName(), scs.GetChannelNumber(), hashValue, videoFileInfo.GetFullFile());
                logWriter.WriteLine($"{DateTime.Now}: Starting Capture (again) on server/channel {scs.GetServerName()}/{scs.GetChannelNumber()}");
                logWriter.WriteLine($"                      {configuration["ffmpegPath"]} {cmdLineArgs}");
                captureProcessInfo = processManager.ExecProcess(logWriter, configuration["ffmpegPath"], cmdLineArgs, (int)timeLeft.TotalMinutes + 1, videoFileInfo.GetFullFile(), recordInfo.cancellationToken, scs.IsBestSelected());
            }
            recordInfo.completedFlag = true;
            logWriter.WriteLine($"{DateTime.Now}: Done Capturing Stream.");

            //Update capture history and save
            TimeSpan finalTimeJustRecorded = DateTime.Now - lastStartedTime;

            channelHistory.GetChannelHistoryInfo(scs.GetChannelNumber()).hoursRecorded += finalTimeJustRecorded.TotalHours;
            channelHistory.GetChannelHistoryInfo(scs.GetChannelNumber()).lastSuccess    = DateTime.Now;
            channelHistory.SetServerAvgKBytesSec(scs.GetChannelNumber(), scs.GetServerName(), captureProcessInfo.avgKBytesSec);
            channelHistory.Save();

            //check if actually done and didn't error out early
            //We assume too many retries as that's really the only way out of the loop (outside of an exception which is caught elsewhere)
            if (DateTime.Now < captureTargetEnd)
            {
                if (recordInfo.cancelledFlag)
                {
                    logWriter.WriteLine($"{DateTime.Now}: Cancelled {recordInfo.description}");
                }
                else
                {
                    logWriter.WriteLine($"{DateTime.Now}: ERROR!  Too many retries - {recordInfo.description}");
                }

                //set partial flag
                recordInfo.partialFlag = true;

                //Send alert mail
                string body = recordInfo.description + " partially recorded due to too many retries or cancellation.  Time actually recorded is " + finalTimeJustRecorded.TotalHours;
                new Mailer().SendErrorMail(configuration, "Partial: " + recordInfo.description, body);
            }
        }