Денис Крешихин

Денис
Крешихин

iOS-разработчик с 15+ летним опытом

Тимлид/сеньор по обстоятельствам

Интересы: swift, uikit, rxswift, oop/ood, devops, agile

2024 © Денис Крешихин

Uploading video to Twitter with Go

Уже почти год на Twitter можно заливать короткие видеоролики, более того возможность загрузки видео доступна из официального API. Конечно, Twitter и раньше позволял отображать ролики из видеохостингов как Youtube и Vimeo, но нужно иметь ввиду, что в отличии от встроенных медиа эти ролики не воспроизводятся автоматически при прокрутке, что привлекает пользователь в меньшей степени.

В этой заметке я опишу как загруждать видео прямиком на Twitter.

Twitter OAuth

Для начала необходимо завести на Twitter свое приложение и получить consumer_key и consumer_secret которые будут необходимы для авторизации через OAuth. Для авторизации через OAuth потребуется подключить библиотеку "github.com/mrjones/oauth" и инициализировать объект consumer:

consumerKey := "YOUR_COSUMER_KEY"
consumerSecret := "YOUR_COSUMER_SECRET"

c := oauth.NewConsumer(
    consumerKey,
    consumerSecret,
    oauth.ServiceProvider{
        RequestTokenUrl:   "https://api.twitter.com/oauth/request_token",
        AuthorizeTokenUrl: "https://api.twitter.com/oauth/authorize",
        AccessTokenUrl:    "https://api.twitter.com/oauth/access_token",
    })

c.Debug(false)

Для возможности загружать видео необходима авторизация пользователя. Что бы автоматизировать этот процесс полезно хранить авторизационные данные от предыдущей сессии в файле (например twitter.json), тогда не будет необходимости переходить в браузер каждый раз:

    accessToken := ReadAccessToken("twitter.json")

    if accessToken == nil {
        return;

        requestToken, u, err := c.GetRequestTokenAndUrl("oob")
        if err != nil {
            log.Fatal(err)
        }

        fmt.Println("(1) Go to: " + u)
        fmt.Println("(2) Grant access, you should get back a verification code.")
        fmt.Println("(3) Enter that verification code here: ")

        verificationCode := ""
        fmt.Scanln(&verificationCode)

        accessToken, err = c.AuthorizeToken(requestToken, verificationCode)
        if err != nil {
            log.Fatal(err)
        }
    }

    client, err := c.MakeHttpClient(accessToken)
    if err != nil {
        panic(err)
    }

Объект client с токеном открывает нам доступ к Twitter API.

##Загрузка видео с Twitter API Мы не можем прикрепить видео непосредственно к сообщению в момент отправки, т.к. для публикации сообщения в Twitter с медиа требуется получить уникальный ID который называется media_id и сперва загрузить видео туда.

Для загрузки видео на Twitter следует выполнить следующие шаги:

  1. Получить уникальный media_id указав длину контента
  2. Загрузить контент
  3. Финализировать контент
  4. Отправить сообщение с указанием media_id

Для загрузки медиа контента и публикации сообщения потребуются два пути Twitter REST API, поэтому полезно определить их как константы модуля. Сама структура Twitter должна хранить как минимум указатель на авторизованный клиент:


package twitter

const StatusUpdate string = "https://api.twitter.com/1.1/statuses/update.json"
const MediaUpload string = "https://upload.twitter.com/1.1/media/upload.json"

type Twitter struct {
    client *http.Client
}

func NewTwitter(client *http.Client) *Twitter {
    self := &Twitter{}
    self.client = client
    return self
}

Инициализация и получение media_id

В момент инициализации мы должны указать два важных параметра: media_type и total_bytes. Эти параметры должны объективно соответствовать параметрам контента, т.к. по ним происходит проверка корретности загрузки. Ошибка в указании типа контента и его длины приводит к ошибки публикации сообщения.

Так же не следует забывать указывать в хедере формат данных сообщения application/x-www-form-urlencoded:

func (self *Twitter) MediaInit(media []byte) (*MediaInitResponse, error) {
    form := url.Values{}
    form.Add("command", "INIT")
    form.Add("media_type", "video/mp4")
    form.Add("total_bytes", fmt.Sprint(len(media)))

    fmt.Println(form.Encode())

    req, err := http.NewRequest("POST",
        MediaUpload, strings.NewReader(form.Encode()))

    req.Header.Add("Content-Type", "application/x-www-form-urlencoded")

    res, err := self.client.Do(req)

    defer res.Body.Close()
    body, err := ioutil.ReadAll(res.Body)
    fmt.Println("response", string(body))

    var mediaInitResponse MediaInitResponse
    err = json.Unmarshal(body, &mediaInitResponse)

    if err != nil {
        return nil, err
    }

    fmt.Println("Initialized media: ", mediaInitResponse);

    return &mediaInitResponse, nil
}

Загрузка контента

Получив корректный ответ с указанным media_id, которое хранит как-правило некоторое большое целочисленно значение, можно приступать к загрузки. Нужно иметь ввиду, что не смотря на ограничение в 15MB загрузать весь контент разом не желательно. Для этого есть две причины.

Во-первых, Twitter выдает ошибку при загрузке блоков больше 5MB поэтому желательно делать размер блока меньше. Во-вторых, передавая большие блоки трудно отследить прогресс загрузки, в случае если такая информация должна отображаться для пользователя.

Для видеоконтента вполне подходит размер блоков в 500KB. Блоки могут быть загружены в произвольном порядке:

func (self *Twitter) MediaAppend(mediaId uint64, media []byte) error {
    step := 500 * 1024
    for s := 0; s * step < len(media); s++ {
        var body bytes.Buffer
        rangeBegining := s * step
        rangeEnd := (s + 1) * step
        if rangeEnd > len(media) {
            rangeEnd = len(media)
        }

        fmt.Println("try to append ", rangeBegining, "-", rangeEnd)

        w := multipart.NewWriter(&body)

        w.WriteField("command", "APPEND")
        w.WriteField("media_id", fmt.Sprint(mediaId))
        w.WriteField("segment_index", fmt.Sprint(s))

        fw, err := w.CreateFormFile("media", "example.mp4")

        fmt.Println(body.String())

        n, err := fw.Write(media[rangeBegining:rangeEnd])

        fmt.Println("len ", n)

        w.Close()

        req, err := http.NewRequest("POST", MediaUpload, &body)

        req.Header.Add("Content-Type", w.FormDataContentType())

        res, err := self.client.Do(req)
        if err != nil {
            return err
        }

        resBody, err := ioutil.ReadAll(res.Body)
        fmt.Println("append response ", string(resBody))
    }

    return nil
}

Особое внимание следует обратить на строчку req.Header.Add("Content-Type", w.FormDataContentType()) т.к. таким способом происходит запись multipart данных в тело запроса. Без этой строчки ничего работать не будет, т.к. Twitter поддерживает загрузку видео только в multipart формате.

Финализация загрузки контента

Получив успешные ответы от загрузки каждого блока можно делать финализацию. Обычно если тип контента или объем данных были некорретны, то ошибка возвращается именно на этом этапе, т.е. до отправки сообщения:

func (self *Twitter) MediaFinilize(mediaId uint64) error {
    form := url.Values{}
    form.Add("command", "FINALIZE")
    form.Add("media_id", fmt.Sprint(mediaId))

    req, err := http.NewRequest("POST",
        MediaUpload, strings.NewReader(form.Encode()))

    req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
    res, err := self.client.Do(req)
    if err != nil {
        return err
    }

    body, err := ioutil.ReadAll(res.Body)
    fmt.Println("final response ", string(body))

    return nil
}

Отправка сообщения с медиа

Итак, успешно завершив загрузку мы можем передать media_id как параметр для нового сообщения. Нужно иметь ввиду, что даже для видео требуется указывать параметр в поле media_ids а не media_id. При этом значение этого поля не обязано хранить массив, там может быть обычная строка, т.е. в формате urlencoded это должно выглядеть примерно как status=text&media_ids=1234567890.

func (self *Twitter) UpdateStatusWithMedia(text string, mediaId uint64) error {
    form := url.Values{}
    form.Add("status", text)
    form.Add("media_ids", fmt.Sprint(mediaId))

    req, err := http.NewRequest("POST",
        StatusUpdate, strings.NewReader(form.Encode()))

    req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
    res, err := self.client.Do(req)
    if err != nil {
        return err
    }

    body, err := ioutil.ReadAll(res.Body)
    fmt.Println("status response ", string(body))

    return nil
}

Что делать если ничего не работает?

  • Проверить что размер видео не превышает 15MB
  • Проверить что тип видео действительно mp4
  • Проверить что длительность видео не превышает 30 секунд
  • Проверить что разрешение видео и частота кадров соответсвуют требованиям Twitter
  • Попробовать загрузить тоже видео через браузер
  • Если видео не грузиться через некоторый браузер, попробовать другой браузер

Требования Twitter описаны здесь: https://dev.twitter.com/rest/public/uploading-media#videorecs

Заключение

Несмотря на то, что такая загрузка кажется довольно сложной, следует иметь ввиду, что многие другие социальные сети крайне не любят когда происходит загрузка видео минуя формы сайта или нативное приложение. В Twiiter же в этом плане намного проще.

Особенный респект разработчикам библиотеки http в Go, т.к. наличие функции w.FormDataContentType() позволяет избежать большой головной боли при формирования тела для multipart запроса.

Весь код вы можете найти на моем Github'е: https://github.com/kreshikhin/twitter-media-uploader

Официальное руководство на английском: https://dev.twitter.com/rest/reference/post/media/upload