Description

회사에서 업무로서 Social Media(Youtube, Instagram, Facebook, Twitter)의 API를 이용해 글 작성, 영상 업로드, 이미지 업로드, 예약 기능, 통계 등과 같은 기능을 파악해야 했다.

Graph API를 이용해 피드를 게시하거나 이미 작성된 게시글 리스트를 가져오는 내용의 블로그 포스팅은 너무 많았지만 영상을 업로드하는 관련 글이 없어서 직접 분석해 작성하게 되었다.

본 포스팅에서는 Video API를 이용해 사용자가 소유한 페이지에 영상 게시글을 게시하는 내용을 작성한다.

 

Progress

이전 포스팅과 마찬가지로 페이지에 게시할 경우에는 페이지 엑세스 토큰이 반드시 필요하다.

사전에 게시할 페이지의 엑세스 토큰을 저장해둔다.

동영상을 업로드하는 API는 두 종류로 나뉜다.

  1. 재개 가능한 업로드 API(Resumable API)
    • 동영상은 10GB / 4시간으로 제한
  2. 재개 불가능한 업로드 API(Non Resumable API)
    • 동영상은 1GB / 20분으로 제한

재개 가능한 업로드(Resumable API)

재개 가능한 업로드의 경우 문서에 나와있는 프로세스는 다음과 같다.

  1. 페이지 또는 그룹에서 업로드 세션을 초기화한다.
  2. 영상의 청크를 분할해 순서대로 업로드한다.
  3. 업로드 세션을 종료한다.

1. 업로드 세션 초기화

사용된 NPM 패키지

  • fs
  • form-data
POST /v12.0/{page-id}/videos ?upload_phase=start &access_token={access-token} &file_size={file-size}
const fs = require('fs');
const FormData = require('form-data');

const uploadSessionInit = async (pageId, filePath, accessToken) => {
  const formData = new FormData();
  const videoStat = fs.statSync(filePath);
  const params = {
    upload_phase: 'start',
    access_token: accessToken,
    file_size: videoStat.size,
  };

  Object.keys(params).forEach((key) => {
    formData.append(key, params[key]);
  });

  const url = `https://graph-video.facebook.com/v12.0/${pageId}/videos`;

  const res = await axios.post(url, formData, {
    headers: {
      ...formData.getHeaders(),
    },
  });

  return res.data;
};

response

{
    "video_id": "2009506465924886",
    "start_offset": "0",
    "end_offset": "1048576",
    "upload_session_id": "2009506472591552"
}

영상 청크 분할 및 분할된 청크 순서대로 업로드

해당 부분에서 가장 애먹었다..

문서에서는 몇 바이트의 청크로 분할하라는 설명도 없고 10485760 바이트를 기준으로 분할하는 예시만 보여준다. 뭐 이건 그렇다치고 그냥 동일한 사이즈로 따라해서 진행하면 된다.

가장 문제인 부분은 각 청크를 업로드 하는 과정에서 아래 이미지를 보면 video_file_chunk라는 필드의 설명에 업로드할 동영상 청크의 이름이라 적혀있다.

다른 Social Media 서비스 API를 모두 써봤지만 업로드 할 때 이름만 적으라는 설명은 처음봤었다. 보통 일반적으로 영상을 업로드하는 API의 경우 video_file_chunk 라는 키 값에 해당 청크의 버퍼를 넣으면 됐었다. 하지만 페이스북의 경우 실제 분할된 파일이 존재한 상태에서 해당 파일의 경로를 적어줘야 했다.

이런 부분때문에 구현 부분도 상당히 애매하다.

동영상 청크 분할

Splitting - Video API - Documentation - Facebook for Developers

공식문서에 어떻게 분할했는지 예시가 나와있다.

MacOS 기준으로 터미널에서 기본 Split Utility를 이용해 분할한다.

약 10메가바이트정도인 10485760바이트 기준으로 분할한다.

해당 명령을 실행하게되면 다음 이미지처럼 xaa, xab, .... 와 같이a, b, c, ... 순으로 알파벳 순으로 네이밍된다.

split -b {chunk-size} {video-file-path}

split -b 10485760 test.mp4

example

const chunkList = async (filePath, size) => {
  const chunks = [];
    // highWaterMark옵션을 이용해 특정 사이즈 기준으로 버퍼를 분할한다.
  const stream = fs.createReadStream(filePath, { highWaterMark: size });

  return new Promise((resolve, reject) => {
    stream.on('error', (err) => {
      resolve(reject);
    });

    stream.on('data', (chunk) => {
      chunks.push(chunk);
    });

    stream.on('end', () => {
      resolve(chunks);
    });
  });
};

response

디버깅해서 분할된 리스트를 캡처하면 다음과 같다.

10485760byte 기준으로 분할된 버퍼 리스트

분할된 청크 업로드

  • pageId: 페이지의 아이디
  • uploadSessionId: 위에서 설명한 uploadSessionInit의 결과 값으로 나온 업로드 세션 아이디
  • accessToken: 페이지 엑세스 토큰
  • chunk: 분할된 청크 리스트에서 업로드 할 개별 청크
  • startOffset: 최초 업로드에는 0을 넣고 이후부터는 이전 요청의 결과 값으로 리턴된 startOffset값을 넣어준다.

video_file_chunk 에 버퍼를 넣어주면 정상적으로 업로드가 되지 않기때문에 fs 모듈을 이용해서 실제 파일을 쓰고 업로드를 모두 마친 이후에 파일을 삭제해주는 방식으로 구현했다.

chunk는 동일한 이름으로 생성해서 교체되게끔 한다.

  1. fs.writeFileSync('chunk', chunk);
  2. video_file_chunk에 fs.createReadStream('chunk') 1번에서 생성한 파일의 스트림을 지정한다.

하나의 개별 청크를 전송하는 함수

const transfer = async (pageId, uploadSessionId, accessToken, chunk, startOffset) => {
  const formData = new FormData();

  fs.writeFileSync('chunk', chunk);

  // formData에 parameter 적용
  const options = {
    upload_phase: 'transfer',
    access_token: accessToken,
    upload_session_id: uploadSessionId,
    start_offset: startOffset,
    video_file_chunk: fs.createReadStream('chunk'),
  };
  Object.keys(options).forEach((key) => {
    formData.append(key, options[key]);
  });

  const url = `https://graph-video.facebook.com/v12.0/${pageId}/videos`;

  const headers = formData.getHeaders();

  const res = await axios.post(url, formData, {
    headers,
    maxBodyLength: Infinity,
    maxContentLength: Infinity,
  });

  return res.data;
};

업로드 세션 종료

모든 청크를 업로드를 완료했으면 업로드 세션을 종료한다.

종료 요청에서는 title, description과 같은 포스팅할 때 필요한 옵션 값들을 넣어주면 된다.

  • title: 게시글 제목
  • description: 영상의 부가설명

그 외에 더 많은 옵션들을 사용하고자하면 다음 링크에서 확인하면 된다.

Graph API Reference /page/videos - Documentation - Facebook for Developers

const finish = async (pageId, title, description, uploadSessionId, startOffset, accessToken) => {
  const formData = new FormData();

  const options = {
    title: title ? title : 'title',
    description: description ? description : 'description',
    upload_phase: 'finish',
    access_token: accessToken,
    upload_session_id: uploadSessionId,
    start_offset: startOffset,
  };

  const url = `https://graph-video.facebook.com/v12.0/${pageId}/videos`;

  Object.keys(options).forEach((key) => {
    formData.append(key, options[key]);
  });

  const headers = formData.getHeaders();

  const res = await axios.post(url, formData, { headers, maxContentLength: Infinity, maxBodyLength: Infinity });

  return res.data;
};

재개 가능한 업로드 컨트롤러 코드

router.post('/videos', async (req, res, next) => {
  try {
    const pageId = '12345678901234';
    const videoPath = 'test.mp4';

    const pageInfo = await facebook.getPageAccessToken(pageId);
    const pageAccessToken = pageInfo.pageAccessToken;

    const session = await facebook.uploadSessionInit(pageId, videoPath, pageAccessToken);
    const sessionId = session.upload_session_id;

    const bufferSize = 10485760;
    const chunks = await util.chunkList(videoPath, bufferSize);

    let startOffset = Number(session.start_offset);
    for await (chunk of chunks) {
      const result = await facebook.transfer(pageId, sessionId, pageAccessToken, chunk, startOffset);
      startOffset = result.start_offset;
    }

    const title = 'test title';
    const description = 'test description';
    const result = await facebook.finish(pageId, title, description, sessionId, startOffset, pageAccessToken);

        // 모든 프로세스가 종료되면 청크 업로드를 위해 만들었던 파일을 rimraf 패키지를 이용해 삭제한다.
    rimraf('chunk', (err) => {
      if (err) {
                ...
      }
    });

    res.send(result);
  } catch (err) {
    console.log(err);
    next(err);
  }
});

재개 불가능한 업로드 (Non Resumable API)

request

POST
https://graph.facebook.com/v12.0/{page-id}/videos

form data

  • access_token
    • 페이지 엑세스 토큰
  • file_url
    • 영상 파일의 URL
  • title
    • 영상 제목
  • description
    • 영상 Description
  • scheduled_publish_time
    • Unix time(10분에서 6개월 사이의 값으로 가능하고 10분 단위의 값이여야 한다)
  • published
    • false(예약 기능을 사용했을 시에 true)

response

{
    "id": "3593897961234958"
}

Reference

동영상 API

Publishing - Video API - Documentation - Facebook for Developers

복사했습니다!