예제 #1
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);

            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);
            }
            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);
            }
        }
예제 #2
0
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            RecordInfo recordInfo = value as RecordInfo;

            //ID
            writer.WriteStartObject();
            writer.WritePropertyName("id");
            serializer.Serialize(writer, recordInfo.id);

            //description
            writer.WritePropertyName("Description");
            serializer.Serialize(writer, recordInfo.description);

            //category
            writer.WritePropertyName("Category");
            serializer.Serialize(writer, recordInfo.category);

            //start date
            writer.WritePropertyName("StartDT");
            serializer.Serialize(writer, recordInfo.GetStartDT().ToString("yy-MM-dd  HH:mm"));

            //start time
            writer.WritePropertyName("StartTime");
            serializer.Serialize(writer, recordInfo.GetStartDT().ToString("HH:mm"));

            //start day of the week
            writer.WritePropertyName("StartDay");
            serializer.Serialize(writer, recordInfo.GetStartDT().ToString("dddd"));

            //end time
            writer.WritePropertyName("EndTime");
            serializer.Serialize(writer, recordInfo.GetEndDT().ToString("HH:mm"));

            //Duration
            writer.WritePropertyName("Duration");
            serializer.Serialize(writer, recordInfo.GetDuration());

            //too many flag
            writer.WritePropertyName("TooManyFlag");
            serializer.Serialize(writer, recordInfo.tooManyFlag);

            //Selected flag
            writer.WritePropertyName("SelectedFlag");
            serializer.Serialize(writer, recordInfo.selectedFlag);

            //Manual flag
            writer.WritePropertyName("ManualFlag");
            serializer.Serialize(writer, recordInfo.manualFlag);

            //Queued flag
            writer.WritePropertyName("QueuedFlag");
            serializer.Serialize(writer, recordInfo.queuedFlag);

            //started flag
            writer.WritePropertyName("StartedFlag");
            serializer.Serialize(writer, recordInfo.captureStartedFlag);

            //Partial flag
            writer.WritePropertyName("PartialFlag");
            serializer.Serialize(writer, recordInfo.partialFlag);

            //Completed flag
            writer.WritePropertyName("CompletedFlag");
            serializer.Serialize(writer, recordInfo.completedFlag);

            //Ignored Flag
            writer.WritePropertyName("CancelledFlag");
            serializer.Serialize(writer, recordInfo.cancelledFlag);

            //Keyword position
            writer.WritePropertyName("KeyWordPos");
            serializer.Serialize(writer, recordInfo.keywordPos);
            writer.WriteEndObject();
        }
예제 #3
0
        public void QueueRecording(ChannelHistory channelHistory, RecordInfo recordInfo, IConfiguration configuration, bool useLogFlag)
        {
            //Write to our very own log as there might be other captures going too
            StreamWriter logWriter = new StreamWriter(Console.OpenStandardOutput());

            if (useLogFlag)
            {
                string     logPath    = Path.Combine(configuration["logPath"], recordInfo.fileName + "Log.txt");
                FileStream fileHandle = new FileStream(logPath, FileMode.OpenOrCreate, FileAccess.Write);
                logWriter = new StreamWriter(fileHandle);
            }
            logWriter.AutoFlush = true;

            //try-catch so we don't crash the whole thing
            try
            {
                //Dump
                logWriter.WriteLine($"{DateTime.Now}: Queuing show: {recordInfo.description} Starting on {recordInfo.GetStartDT()} for {recordInfo.GetDuration()} minutes ({recordInfo.GetDuration() / 60}hrs ish)");

                //Wait here until we're ready to start recording
                if (recordInfo.strStartDT != null)
                {
                    TimeSpan oneHour    = new TimeSpan(1, 0, 0);
                    DateTime recStart   = recordInfo.GetStartDT();
                    TimeSpan timeToWait = recStart - DateTime.Now;
                    logWriter.WriteLine($"{DateTime.Now}: Starting recording at {recStart} - Waiting for {timeToWait.Days} Days, {timeToWait.Hours} Hours, and {timeToWait.Minutes} minutes.");

                    while (timeToWait.Seconds >= 0 && DateTime.Now < recStart && !recordInfo.cancelledFlag)
                    {
                        if (timeToWait > oneHour)
                        {
                            timeToWait = oneHour;
                        }

                        if (timeToWait.Seconds >= 0)
                        {
                            recordInfo.mre.WaitOne(timeToWait);
                            mre.Reset();
                            logWriter.WriteLine($"{DateTime.Now}: Waking up to check...");
                        }

                        timeToWait = recStart - DateTime.Now;
                    }

                    if (recordInfo.cancelledFlag)
                    {
                        logWriter.WriteLine($"{DateTime.Now}: Cancelling due to request");
                        recordInfo.queuedFlag         = false;
                        recordInfo.processSpawnedFlag = false;
                        return;
                    }
                }

                //Authenticate
                Task <string> authTask  = Authenticate();
                string        hashValue = authTask.Result;
                if (string.IsNullOrEmpty(hashValue))
                {
                    Console.WriteLine($"ERROR: Unable to authenticate.  Check username and password?");
                    Environment.Exit(1);
                }

                //Get latest channels (Channels may have changed since the show was queued.  Exception is thrown if time has changed, or no longer there)
                logWriter.WriteLine($"{DateTime.Now}: Grabbing latest channels");
                new Schedule().RefreshChannelList(configuration, recordInfo);

                //We need to manage our resulting files
                VideoFileManager videoFileManager = new VideoFileManager(configuration, logWriter, recordInfo.fileName);

                //Set capture started flag
                recordInfo.captureStartedFlag = true;

                //Capture stream
                CaptureStream(logWriter, hashValue, channelHistory, recordInfo, videoFileManager);

                //Let's take care of processing and publishing the video files
                videoFileManager.ConcatFiles();
                videoFileManager.MuxFile(recordInfo.description);
                videoFileManager.PublishAndCleanUpAfterCapture(recordInfo.category, recordInfo.preMinutes);

                //Cleanup
                logWriter.WriteLine($"{DateTime.Now}: Done Capturing");
                logWriter.Dispose();

                //Send alert mail
                if (recordInfo.emailFlag)
                {
                    new Mailer().SendShowReadyMail(configuration, recordInfo);
                }
            }
            catch (Exception e)
            {
                logWriter.WriteLine("======================");
                logWriter.WriteLine($"{DateTime.Now}: ERROR - Exception!");
                logWriter.WriteLine("======================");
                logWriter.WriteLine($"{e.Message}\n{e.StackTrace}");

                //Send alert mail
                string body = recordInfo.description + " failed with Exception " + e.Message;
                body = body + "\n" + e.StackTrace;
                new Mailer().SendErrorMail(configuration, "StreamCapture Exception! (" + e.Message + ")", body);
            }
        }
예제 #4
0
        public static void Main(string[] args)
        {
            //Deal with command line
            CommandLineApplication commandLineApplication = new CommandLineApplication(throwOnUnexpectedArg: false);
            CommandOption          channels = commandLineApplication.Option("-c | --channels", "Channels to record in the format nn+nn+nn (must be 2 digits)", CommandOptionType.SingleValue);
            CommandOption          duration = commandLineApplication.Option("-d | --duration", "Duration in minutes to record", CommandOptionType.SingleValue);
            CommandOption          filename = commandLineApplication.Option("-f | --filename", "File name (no extension)", CommandOptionType.SingleValue);
            CommandOption          datetime = commandLineApplication.Option("-d | --datetime", "Datetime MM/DD/YY HH:MM (optional)", CommandOptionType.SingleValue);
            CommandOption          test     = commandLineApplication.Option("-t | --test", "Does a dryrun based on keywords.json (optional)", CommandOptionType.SingleValue);

            commandLineApplication.HelpOption("-? | -h | --help");
            commandLineApplication.Execute(args);

            //Welcome message
            Console.WriteLine($"{DateTime.Now}: StreamCapture Version 2.03 5/16/2017");

            //Read and build config
            var builder       = new ConfigurationBuilder().AddJsonFile("appsettings.json");
            var configuration = builder.Build();

            VerifyAppsettings(configuration);

            //do we have optional args passed in?
            bool optionalArgsFlag = false;

            if (channels.HasValue() || duration.HasValue() || filename.HasValue())
            {
                VerifyCommandLineParams(channels, duration, filename, datetime);
                optionalArgsFlag = true;
            }
            else
            {
                Console.WriteLine($"{DateTime.Now}: Using keywords.json to search schedule. (Please run with --help if you're confused)");
                Console.WriteLine($"=======================");
            }

            //Use optional parameters to record are passed in
            if (optionalArgsFlag)
            {
                //Create new RecordInfo
                RecordInfo recordInfo = new RecordInfo();
                recordInfo.channels = new Channels();
                recordInfo.channels.LoadChannels(channels.Value());
                recordInfo.strDuration        = duration.Value();
                recordInfo.strStartDT         = datetime.Value();
                recordInfo.fileName           = filename.Value();
                recordInfo.processSpawnedFlag = true;
                recordInfo.mre = new ManualResetEvent(false);
                recordInfo.cancellationTokenSource = new CancellationTokenSource();
                recordInfo.cancellationToken       = recordInfo.cancellationTokenSource.Token;

                //Record a single show and then quit
                ChannelHistory channelHistory = new ChannelHistory();
                Recorder       recorder       = new Recorder(configuration);
                recorder.QueueRecording(channelHistory, recordInfo, configuration, false);
                Environment.Exit(0);
            }
            else if (test.HasValue())
            {
                //grab schedule and do a dryrun based on keywords.json to see what would happen, but don't actually do it
                Recorder recorder = new Recorder(configuration);
                recorder.DryRun();
                Environment.Exit(0);
            }
            else
            {
                //Monitor schedule and spawn capture sessions as needed
                Console.WriteLine($"{DateTime.Now}: Starting monitor mode...");
                Recorder recorder = new Recorder(configuration);
                recorder.MonitorMode();
            }
        }