public static async Task Run(
            [QueueTrigger("%AzureStorageConversionQueueName%", Connection = "AzureWebJobsStorage")] string message,
            [SignalR(HubName = "broadcast")] IAsyncCollector <SignalRMessage> signalRMessages,
            ExecutionContext context,
            ILogger log)
        {
            var sw = new Stopwatch();

            sw.Start();
            log.LogInformation("Downloader triggered");

            var payload = JsonConvert.DeserializeObject <QueueMessagePayload>(message);

            AppInsightsClient.SetOperation(context.InvocationId.ToString(), "downloader").SetSessionId(payload.ClientId);

            var video = payload.Video;

            video.DownloaderInvocationId = context.InvocationId;
            var    cts                = new CancellationTokenSource();
            string videoTempPath      = null;
            string audioTempPath      = null;
            int?   previousPercentage = null;
            long?  cliDuration        = null;

            bool Publish(string s)
            {
                if (Cancellation.Tokens.ContainsKey(video.DownloaderInvocationId))
                {
                    video.Error = "Download cancelled";
                    if (!cts.IsCancellationRequested)
                    {
                        cts.Cancel();
                    }
                    return(false);
                }
                video.Message = s;
                signalRMessages.Publish("inprogress", video); //payload.ClientId
                return(true);
            }

            void ProgressNotifier(string s)
            {
                if (Cancellation.Tokens.ContainsKey(video.DownloaderInvocationId))
                {
                    cts.Cancel();
                    return;
                }

                var m = FfmpegStatus.Match(s);

                if (m.Success && TimeSpan.TryParse(m.Groups["time"].Value, CultureInfo.InvariantCulture, out var time))
                {
                    var p = (int)Math.Floor(time.TotalMilliseconds * 100 / video.Duration.TotalMilliseconds);
                    if (previousPercentage == p)
                    {
                        return;
                    }
                    Publish($"{CONVERTING} ({p}%)");
                    previousPercentage = p;
                }
            }

            try
            {
                MediaStreamInfo streamInfo = null;
                if (Publish("Processing..."))
                {
                    streamInfo = await ProcessVideo(video, log);
                }

                if (Publish("Downloading..."))
                {
                    videoTempPath = await DownloadVideo(streamInfo, video, log, cts.Token);
                }

                if (Publish(CONVERTING))
                {
                    var result = await ConvertToAudio(video, videoTempPath, log, ProgressNotifier, cts.Token);

                    cliDuration   = result.Item1.RunTime.Ticks;
                    audioTempPath = result.Item2;
                    WriteId3Tag(video, audioTempPath, log);
                }

                if (Publish("Storing..."))
                {
                    await UploadAudio(video, audioTempPath, log, cts.Token);
                }
            }
            catch (TaskCanceledException ex)
            {
                log.LogInformation("Download cancelled");
                video.Error = ex.Message;
                AppInsightsClient.TrackException(ex, video.Properties);
            }
            catch (Exception ex)
            {
                log.LogError(ex, ex.Message.EscapeCurlyBraces());
                video.Error = ex.Message;
                AppInsightsClient.TrackException(ex, video.Properties);
            }
            finally
            {
                Cancellation.Tokens.TryRemove(video.DownloaderInvocationId, out var value);
                cts.Dispose();
                if (videoTempPath != null && File.Exists(videoTempPath))
                {
                    File.Delete(videoTempPath);
                }
                if (audioTempPath != null && File.Exists(audioTempPath))
                {
                    File.Delete(audioTempPath);
                }
                Publish(string.Empty);
                AppInsightsClient.TrackEvent("Download", video.Properties,
                                             new Dictionary <string, double>
                {
                    { "Video duration", video.Duration.Ticks },
                    { "Function duration", sw.Elapsed.Ticks },
                    { "Conversion duration", cliDuration ?? 0 },
                    { "Cancelled", cts.IsCancellationRequested ? 1 : 0 }
                });
            }
        }