1

I have this function that basically uploads a video to YouTube. However, currently I am having to hard code the actual file path where the video is located(example: @"C:\Users\Peter\Desktop\audio\test.mp4";). I am wondering is there a way to make this more dynamic. For example right now I am using HTTP trigger, but how can the code be refactored to make it a Blob trigger? So that, when I upload a new .mp4 file into my blob storage container, this function would get triggered.

I am asking this because, I planning to move this function to Azure portal and there, I won't be able to specify local file path there like I am doing right now. Thanks in advance.

using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Google.Apis.Auth.OAuth2;
using Google.Apis.Upload;
using Google.Apis.YouTube.v3.Data;
using System.Reflection;
using Google.Apis.YouTube.v3;
using Google.Apis.Services;
using System.Threading;

namespace UploadVideoBlob
{
    public static class Function1
    {
        [FunctionName("Function1")]
        public static async Task Run([BlobTrigger("video/{name}")]Stream myBlob, string name, Microsoft.Azure.WebJobs.ExecutionContext context, ILogger log)
        {
            UserCredential credential;
            using(var stream = new FileStream(System.IO.Path.Combine(context.FunctionDirectory, "client_secrets.json"), FileMode.Open, FileAccess.Read))
            {
                credential = await GoogleWebAuthorizationBroker.AuthorizeAsync(
                    GoogleClientSecrets.Load(stream).Secrets,
                    new[] { YouTubeService.Scope.YoutubeUpload },
                    "user",
                    CancellationToken.None
                );
            }

            var youtubeService = new YouTubeService(new BaseClientService.Initializer()
            {
                HttpClientInitializer = credential,
                ApplicationName = Assembly.GetExecutingAssembly().GetName().Name
            });

            var video = new Video();
            video.Snippet = new VideoSnippet();
            video.Snippet.Title = "Default Video Title";
            video.Snippet.Description = "Default Video Description";
            video.Snippet.Tags = new string[] { "tag1", "tag2" };
            video.Snippet.CategoryId = "22";
            video.Status = new VideoStatus();
            video.Status.PrivacyStatus = "unlisted";
            var VideoInsertRequest = youtubeService.Videos.Insert(video, "snippet,status", myBlob, "video/*");
            await VideoInsertRequest.UploadAsync();
        }
    }
}

function.json

{
  "generatedBy": "Microsoft.NET.Sdk.Functions-1.0.29",
  "configurationSource": "attributes",
  "bindings": [
    {
      "type": "blobTrigger",
      "path": "video/{name}",
      "name": "myBlob"
    }
  ],
  "disabled": false,
  "scriptFile": "../bin/UploadVideoBlob.dll",
  "entryPoint": "UploadVideoBlob.Function1.Run"
}

client_secrets.json

{
  "installed": {
    "client_id": "147300761218-dl0rhktkoj8arh0ebu5pu56es06hje5p.apps.googleusercontent.com",
    "project_id": "mytestproj",
    "auth_uri": "https://accounts.google.com/o/oauth2/auth",
    "token_uri": "https://oauth2.googleapis.com/token",
    "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
    "client_secret": "xxxxxxxxxxxxxxxxxx",
    "redirect_uris": [ "urn:ietf:wg:oauth:2.0:oob"]
  }
}

2 Answers 2

2

I had some spare time, and despite using Azure Functions for more than a year now, never had the chance to implement an actual BlobTrigger. So I thought I'd share my tiny core 3.0 test implementation, and add an overkill answer as a supplement to @Bryan Lewis' correct answer.

If you want to test this before launching on Azure, you should first make sure you have Azure Storage Emulator. If you have Visual Studio 2019, it should already be installed. If you have VS19 but it's not installed, you should open the Visual Studio Installer and modify your VS19 installation. Under "Individual Components", you should be able to find "Azure Storage Emulator". If you don't have VS19, you can get it here.

Next I'd recommend downloading Azure Storage Explorer from here. If the emulator is running and you didn't change the default ports for the Storage Emulator you should be able to find a default entry under Local & Attached > Storage Accounts > (Emulator - Default ports).

Using the Storage Explorer, you can expand "Blob Containers". Right click "Blob Containers", and choose to "Create Blob Container", and give it a name. For my example I named it "youtube-files". I also created another container calling it "youtube-files-descriptions".

Create container

Now for the actual function. I gave myself the liberty to do this with dependency injection (I just dread the static chaos). For this you'll have to include the NuGet package Microsoft.Azure.Functions.Extensions and Microsoft.Extensions.DependencyInjection.

Startup
We register our services and what-not here. I'll add an InternalYoutubeService (named as such to not confuse it with the one supplied by the Goodle APIs). You can read more about DI and Azure functions here.

// Notice that the assembly definition comes before the namespace
[assembly: FunctionsStartup(typeof(FunctionApp1.Startup))]
namespace FunctionApp1
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
            // You can of course change service lifetime as needed
            builder.Services.AddTransient<IInternalYoutubeService, InternalYoutubeService>();
        }
    }
}

BlobFunction
You don't have to add/register classes containing any azure functions, they're handled automagically. Notice the difference between BlobTrigger and Blob. BlobTrigger dictates that it is only when files are uploaded into the "youtube-files" container that the function actually triggers; simultaneously it will look up a blob in the "youtube-files-descriptions" container for a file with the same filename as the incoming one from the BlobTrigger, but with a "-description" suffix and only if it is using ".txt" extension. If the blob is not found, it will return null, and the bound string description will be null. You can find the different availing bindings here. The link will generally speaking tell you what you need to know about the BlobTrigger and Blob attribute.

[StorageAccount("AzureWebJobsStorage")]
public class BlobFunction
{
    private readonly IInternalYoutubeService _YoutubeService;
    private readonly ILogger _Logger;

    // We inject the YoutubeService    
    public BlobFunction(IInternalYoutubeService youtubeService, ILogger<BlobFunction> logger)
    {
        _YoutubeService = youtubeService;
        _Logger = logger;
    }

    [FunctionName("Function1")]
    public async Task Run(
        [BlobTrigger("youtube-files/{filename}.{extension}")] Stream blob,
        [Blob("youtube-files-descriptions/{filename}-description.txt")] string description,
        string filename,
        string extension)
    {
        switch (extension)
        {
            case "mp4":
                await _YoutubeService.UploadVideo(blob, filename, description, "Some tag", "Another tag", "An awesome tag");
                break;

            case "mp3":
                await _YoutubeService.UploadAudio(blob, filename, description);
                break;

            default:
                _Logger.LogInformation($"{filename}.{extension} not handled");
                break;
        }
    }
}

YoutubeService
Will contain the logic that'll handle the actual authentication (the OAuth2 you're using) and the upload of the file. You can refer to @Bryan Lewis' answer in terms of how to use the incoming Stream. We could store our credentials in our function app's configuration and inject the IConfiguration interface, which allows us to access the values by supplying the value's key defined in the configuration. This way you avoid hardcoding any credentials in your code. I have omitted the YouTube-specific upload logic, as I have no experience with the library you're using, but it should be simple enough to migrate the logic to the service.

public interface IInternalYoutubeService
{
    Task UploadVideo(Stream stream, string title, string description, params string[] tags);
    Task UploadAudio(Stream stream, string title, string description, params string[] tags);
}

internal class InternalYoutubeService : IInternalYoutubeService
{
    private readonly IConfiguration _Configuration;
    private readonly ILogger _Logger;

    public InternalYoutubeService(IConfiguration configuration, ILogger<InternalYoutubeService> logger)
    {
        _Configuration = configuration;
        _Logger = logger;
    }

    public async Task UploadAudio(Stream stream, string title, string description, params string[] tags)
    {
        _Logger.LogInformation($"{_Configuration["YoutubeAccountName"]}");
        _Logger.LogInformation($"{_Configuration["YoutubeAccountPass"]}");
        _Logger.LogInformation($"Bytes: {stream.Length} - {title} - {description} - {string.Join(", ", tags)}");
    }

    public async Task UploadVideo(Stream stream, string title, string description, params string[] tags)
    {
        _Logger.LogInformation($"{_Configuration["YoutubeAccountName"]}");
        _Logger.LogInformation($"{_Configuration["YoutubeAccountPass"]}");
        _Logger.LogInformation($"Bytes: {stream.Length} - {title} - {description} - {string.Join(", ", tags)}");
    }
}

local.settings.json
You'd of course put these values into your function app's configuration on Azure Portal, except the storage string, when you're done testing locally.

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet",
    "YoutubeAccountName": "MyAccountName",
    "YoutubeAccountPass": "MySecretPassword"
  }
}

Testing
I'm testing with a simple text file, "Test File-description.txt", containing the text "This is a sample description.". I also have a ~5MB MP3 file, "Test File.mp3". I start by drag-n-dropping my text file into the "youtube-files-descriptions" container, followed by drag-n-dropping the "Test File.mp3" file into the "youtube-files" container. The function is not triggered by uploading the text file; it's not 'till I upload "Test File.mp3" that the function triggers. I see the following lines logged:

Executing 'Function1' (Reason='New blob detected: youtube-files/Test File.mp3', Id=50a50657-a9bb-41a5-a7d5-2adb84477f69)
MyAccountName
MySecretPassword
Bytes: 5065849 - Test File - This is a sample description. -
Executed 'Function1' (Succeeded, Id=50a50657-a9bb-41a5-a7d5-2adb84477f69)
Sign up to request clarification or add additional context in comments.

1 Comment

thank you for detailed response. This helped a lot and gave clarity to my understanding of Azure functions. I was looking for a way to bring in YouTube API to upload videos automatically to my youtube channel. One blocker along the way was authentication using Azure.But your explanation makes sense. Thanks again.
0

It's a pretty straight forward change. Just swap in the Blob Trigger. Replace:

public static async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req, ILogger log)

with

public static void Run([BlobTrigger("blob-container-name/{fileName}")] Stream videoBlob, string fileName, ILogger log)

This give you a stream (videoBlob) to work with (and the video's filename if you need it). Then substitute this new stream for your FileStream. It appears that you used the Google/YouTube example code to construct your function, but there is not much need to create a separate Run() method for an Azure function. You could simplify things but combining your Run() method into the main function code instead of calling "await Run();".

Change your function to this:

using Google.Apis.Auth.OAuth2;
using Google.Apis.Services;
using Google.Apis.YouTube.v3;
using Google.Apis.YouTube.v3.Data;
using Microsoft.Azure.WebJobs;
using Microsoft.Extensions.Logging;
using System.IO;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;


namespace UploadVideoBlob
{
    public static class Function1
    {
        [FunctionName("Function1")]
        public static async Task Run([BlobTrigger("video/{name}", 
            Connection = "DefaultEndpointsProtocol=https;AccountName=uploadvideoblob;AccountKey=XXXX;EndpointSuffix=core.windows.net")]Stream videoBlob, string name,
            Microsoft.Azure.WebJobs.ExecutionContext context, ILogger log)
        {
            UserCredential credential;
            using (var stream = new FileStream(System.IO.Path.Combine(context.FunctionDirectory, "client_secrets.json"), FileMode.Open, FileAccess.Read))
            {
                credential = await GoogleWebAuthorizationBroker.AuthorizeAsync(
                    GoogleClientSecrets.Load(stream).Secrets,
                    // This OAuth 2.0 access scope allows an application to upload files to the
                    // authenticated user's YouTube channel, but doesn't allow other types of access.
                    new[] { YouTubeService.Scope.YoutubeUpload },
                    "user",
                    CancellationToken.None
                );
            }

            var youtubeService = new YouTubeService(new BaseClientService.Initializer()
            {
                HttpClientInitializer = credential,
                ApplicationName = Assembly.GetExecutingAssembly().GetName().Name
            });

            var video = new Video();
            video.Snippet = new VideoSnippet();
            video.Snippet.Title = "Default Video Title";
            video.Snippet.Description = "Default Video Description";
            video.Snippet.Tags = new string[] { "tag1", "tag2" };
            video.Snippet.CategoryId = "22"; // See https://developers.google.com/youtube/v3/docs/videoCategories/list
            video.Status = new VideoStatus();
            video.Status.PrivacyStatus = "unlisted"; // or "private" or "public"
            var videosInsertRequest = youtubeService.Videos.Insert(video, "snippet,status", videoBlob, "video/*");
            await videosInsertRequest.UploadAsync();
        }
    }
}

Using the ProgressChanged and ResponseReceived events is not really needed for a Function except just for logging, so you could keep those or eliminate them. The Google example is a console app, so it's outputting a lot of status to the console.

I also made one additional change to correct for the file path of your "client_secrets.json". This code assumes that json file is in the same directory as your function and is being published with the Function.

18 Comments

Hi @Bryan Lewis, One thing that I am still confused about is how to handle "filePath" for a blob storage? For example, if I add a new .mp4 in my Blog Storage container, how can I pass that file as an input parameter in the function? Currently as you can tell I have hard coded the file path, just to test it on my local. But when I move the function to the Azure portal I won't be able to use the hard coded way.
You're not using a filePath at all. You're removing the filePath variable and the FileStream using statement, because the blob trigger "gives" you a stream object to play with. That stream is the file from blob storage. I updated my answer a bit to clarify.
One important thing to test is file size and Function timeouts. If your videos are large (multi gigabyte), then you need to realize that you are copying the blob from blob storage to the Azure Function VM and then uploading it to YouTube. All of that copying takes time and Azure Functions have a 10 minute timeout on the Consumption Plan. So test if you have large files. You may not run into these issues using the Azure Function runtime on your local machine.
if you don't mind, could you please put the pieces of code that you had demoed above into one? For some reason it's breaking on my end. Thanks.
Half correct. Your assumption about the container is correct (it's looking in the "video" container). But the connection string is wrong (sorry, I didn't catch that earlier). The value for "Connection" in the function is a reference to the name of a app settings variable that actually contains the connection string. So this could be Connection = "StorageConnectionString" and then create a variable called "StorageConnectionString" in your local.settings.json that has your actual connection string in it. The default name is "AzureWebJobsStorage", so you could use that too.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.