//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); } }
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(); }
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); } }
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(); } }