Expo React-Native Youtube 视频上传使用 Fetch()

Expo React-Native Youtube video upload using Fetch()

我正在尝试使用 V3 Youtube.video.insert API 方法在 youtube 上上传视频。当我调用该方法时,我收到以下错误消息:Bad request: Request contains an invalid argument.。尽管出现错误消息,我上传的内容仍显示在我的个人 YouTube 帐户中的“我的视频”下。我是 React Native 的新手,我很难理解 Youtube API 文档,有人可以向我解释我做错了什么或者我该如何解决吗?

这是我当前的请求:

let response = await fetch(
        'https://youtube.googleapis.com/youtube/v3/videos?key=' + API_KEY,
        {
          method: 'POST',
          headers: {
            'Authorization': 'Bearer ' + accessToken,
            'Accept': 'application/json',
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({
            part: 'id,snippet,status',
            notifySubscribers: false,
            requestBody: {
              snippet: {
                title: 'YouTube Upload Test',
                description: 'Testing YouTube upload',
              },
              status: {
                privacyStatus: 'private',
              },
            },
            media: {
              body: 'file:///data/user/0/host.exp.exponent/cache/ExperienceData/Camera/video.mp4',
            }
          })
        }
      );

我尝试从 body: 中取出所有内容,但我得到了相同的回复。

以下是我试图理解的链接: https://developers.google.com/youtube/v3/docs/videos/insert https://github.com/googleapis/google-api-nodejs-client/blob/master/samples/youtube/upload.js

更新:

好的,我想我明白了,但我仍然不知道如何附加视频文件...现在这是我的代码:

let response = await fetch(
    'https://youtube.googleapis.com/youtube/v3/videos?part=snippet&part=status&key=' + API_KEY,
    {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer ' + accessToken,
        'Accept': 'application/json',
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        snippet: {
          title: "This is the title",
          description: "This is the description.",
        },
        status: {
          privacyStatus: 'private',
        }
      }),          
    }
  );

尝试使用我自己的 YouTube 帐户后,我遇到了同样的问题。

Youtube 的文档也不是很清楚。 This 问题与你的问题类似,但是这个人使用了 axios

他说他花了几个月的时间,但没弄清楚。之后他使用了 npm 中的 youtube-video-apihttps://www.npmjs.com/package/youtube-video-api.

我认为您应该转向其他解决方案,不要拘泥于此代码。

喜欢你可以托管一个node.js后端,然后将你的视频和视频信息发送到后端上传到youtube供应用用户使用。

我解决了!我从 YouTube API 中获取了 corse_upload.js 示例并在 React Native / Expo 中实现了它。

这是我正在使用的 React Native 中的 API.js

import React, { Component } from 'react';

export default class API extends Component {

    constructor(props) {
        super(props);

        const obj = this;    
        const DRIVE_UPLOAD_URL = 'https://www.googleapis.com/upload/drive/v2/files/';    

        var options = props;

        var noop = function() {};
        this.file = options.file;
        this.contentType = options.contentType || this.file.type || 'application/octet-stream';
        this.metadata = options.metadata || {
            'title': this.file.name,
            'mimeType': this.contentType
        };
        this.token = options.token;
        this.onComplete = options.onComplete || noop;
        this.onProgress = options.onProgress || noop;
        this.onError = options.onError || noop;
        this.offset = options.offset || 0;
        this.chunkSize = options.chunkSize || 0;
        //this.retryHandler = new RetryHandler();
        this.retryHandler = new obj.RetryHandler();

        this.url = options.url;
        if (!this.url) {
            var params = options.params || {};
            params.uploadType = 'resumable';
            //this.url = this.buildUrl_(options.fileId, params, options.baseUrl);
            this.url = obj.buildUrl_(options.fileId, params, options.baseUrl);
        }
        this.httpMethod = options.fileId ? 'PUT' : 'POST';    
    }

    RetryHandler = function() {
        this.interval = 1000; // Start at one second
        this.maxInterval = 60 * 1000; // Don't wait longer than a minute 
    };

    retry = function(fn) {
        setTimeout(fn, this.interval);
        this.interval = this.nextInterval_();
    };

    reset = function() {
        this.interval = 1000;
    };

    nextInterval_ = function() {
        var interval = this.interval * 2 + this.getRandomInt_(0, 1000);
        return Math.min(interval, this.maxInterval);
    };

    getRandomInt_ = function(min, max) {
        return Math.floor(Math.random() * (max - min + 1) + min);
    };

    buildQuery_ = function(params) {
        params = params || {};
        return Object.keys(params).map(function(key) {
            return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]);
        }).join('&');
    };

    buildUrl_ = function(id, params, baseUrl) {
        var url = baseUrl || DRIVE_UPLOAD_URL;
        if (id) {
            url += id;
        }
        var query = this.buildQuery_(params);
        if (query) {
            url += '?' + query;
        }
        return url;
    }; 

    upload = function() {
        //var self = this;
        var xhr = new XMLHttpRequest();  
        xhr.open(this.httpMethod, this.url, true);
        xhr.setRequestHeader('Authorization', 'Bearer ' + this.token);
        xhr.setRequestHeader('Content-Type', 'application/json');
        xhr.setRequestHeader('X-Upload-Content-Length', this.file.size);
        xhr.setRequestHeader('X-Upload-Content-Type', this.contentType);
    
        xhr.onload = function(e) {
            if (e.target.status < 400) {
            var location = e.target.getResponseHeader('Location');
            this.url = location;
            this.sendFile_();
            } else {
            this.onUploadError_(e);
            }
        }.bind(this);
        xhr.onerror = this.onUploadError_.bind(this);
        xhr.send(JSON.stringify(this.metadata));
    };

    sendFile_ = function() {
        var content = this.file;
        console.log(content);
        var end = this.file.size;
    
        if (this.offset || this.chunkSize) {
            // Only bother to slice the file if we're either resuming or uploading in chunks
            if (this.chunkSize) {
            end = Math.min(this.offset + this.chunkSize, this.file.size);
            }
            content = content.slice(this.offset, end);
            console.log(content);
        }
    
        var xhr = new XMLHttpRequest();
        xhr.open('PUT', this.url, true);
        xhr.setRequestHeader('Content-Type', this.contentType);
        xhr.setRequestHeader('Content-Range', 'bytes ' + this.offset + '-' + (end - 1) + '/' + this.file.size);
        xhr.setRequestHeader('X-Upload-Content-Type', this.file.type);
        if (xhr.upload) {
            xhr.upload.addEventListener('progress', this.onProgress);
        }
        xhr.onload = this.onContentUploadSuccess_.bind(this);
        xhr.onerror = this.onContentUploadError_.bind(this);
        xhr.send(content);
        console.log(content);
    };

    resume_ = function() {
        var xhr = new XMLHttpRequest();
        xhr.open('PUT', this.url, true);
        xhr.setRequestHeader('Content-Range', 'bytes */' + this.file.size);
        xhr.setRequestHeader('X-Upload-Content-Type', this.file.type);
        if (xhr.upload) {
            xhr.upload.addEventListener('progress', this.onProgress);
        }
        xhr.onload = this.onContentUploadSuccess_.bind(this);
        xhr.onerror = this.onContentUploadError_.bind(this);
        xhr.send();
    };

    extractRange_ = function(xhr) {
        var range = xhr.getResponseHeader('Range');
        if (range) {
            this.offset = parseInt(range.match(/\d+/g).pop(), 10) + 1;
        }
    };

    onContentUploadSuccess_ = function(e) {
        if (e.target.status == 200 || e.target.status == 201) {
            this.onComplete(e.target.response);
        } else if (e.target.status == 308) {
            this.extractRange_(e.target);
            this.reset();
            this.sendFile_();
        }
    };

    onContentUploadError_ = function(e) {
        if (e.target.status && e.target.status < 500) {
            this.onError(e.target.response);
        } else {
            this.retry(this.resume_.bind(this));
        }
    };

    onUploadError_ = function(e) {
        this.onError(e.target.response); // TODO - Retries for initial upload
    };

}

这是我在App.js中使用它的方式:

import YTDAPI from './assets/API'
import RNFS from 'react-native-fs';

uploadMediaToYouTube = async function(accessToken, videoUri) {                
    const fileInfo = await RNFS.stat(videoUri);
    var file = {
      name: fileInfo.path.split('/').pop(),
      size: fileInfo.size,
      uri: fileInfo.path,
      type: 'video/mp4'
    }  

    var metadata = {
      snippet: {
        title: 'This is a new title',
        description: 'This is a new description',
        tags: ['youtube-cors-upload'],
        categoryId: 22
      },
      status: {
        privacyStatus: 'unlisted'
      }
    };

    var uploader = new YTDAPI({
      baseUrl: 'https://www.googleapis.com/upload/youtube/v3/videos',
      file: file,
      token: this.accessToken,
      metadata: metadata,
      params: {
        part: Object.keys(metadata).join(',')
      },
      onError: function(data) {
        console.log(data);
        var message = data;
        try {
        var errorResponse = JSON.parse(data);
        message = errorResponse.error.message;
        } finally {
        alert(message);
        }
      }.bind(this),
      onProgress: function(data) {
        var currentTime = Date.now();
        var bytesUploaded = data.loaded;
        var totalBytes = data.total;
        var bytesPerSecond = bytesUploaded / ((currentTime - window.uploadStartTime) / 1000);
        var estimatedSecondsRemaining = (totalBytes - bytesUploaded) / bytesPerSecond;
        var percentageComplete = (bytesUploaded * 100) / totalBytes;
        console.log("Uploaded: " + bytesUploaded + " | Total: " + totalBytes + " | Percentage: " + percentageComplete + " | Esitmated seconds remaining: " + estimatedSecondsRemaining);        
      }.bind(this),
      onComplete: function(data) {
        console.log("Complete");
      }.bind(this)
    });

    window.uploadStartTime = Date.now();
    uploader.upload();
  }

为此,您需要在 Google Cloud Console 上配置 API 密钥、Web ClientId 和 oauth2 同意屏幕,并允许 Youtube Data V3 API 库并验证一个用户得到一个 access_token ,你将传递给我的函数。

更新

使用 Fetch() 获取文件会导致应用程序因大文件而崩溃,因为它会尝试将整个视频文件加载到内存中。我使用 react-native-fs 解决了这个问题。我更新了上面的代码。