To post large (>15 MB, >30 second) videos using the Twitter API, you need to use their chunked media upload API rather than the basic media upload API. This is a little more complicated, but thankfully it's still pretty straightforward. In this post I'll be using the Codebird library - to get it to work, you'll need to use the release-3.2.0 branch, so add the following to the require section of your composer.json file:

"jublonet/codebird-php": "dev-release/3.2.0"

First, import Codebird and initalize it with your Twitter credentials. If you're tweeting on behalf of a user, be sure to use their access token and access token secret. Also, set the request timeout to something high - we'll be uploading in chunks of 4 megabytes, so the standard 10 second timeout may be a little low.

Codebird::setConsumerKey(TWITTER_CLIENT_ID, TWITTER_CLIENT_SECRET);
$twitter = Codebird::getInstance();
$twitter->setToken(TWITTER_ACCESS_TOKEN, TWITTER_ACCESS_TOKEN_SECRET);
$twitter->setTimeout(60 * 1000); // 60 second request timeout

Next, get a handle to the file you want to upload and get its size.

$file = fopen(VIDEO_FILENAME, 'rb');
$size = fstat($file)['size'];

Now you're ready to use the chunked media upload API. You should add some validation to test that the file you are uploading is less than 512MB in size and less than 140 seconds in duration. The first command you need to run is the INIT command. It's important that you send the media_category field in this request, otherwise you won't be able to upload large videos (over 15 megabytes and 30 seconds).

$media = $twitter->media_upload([
    'command' => 'INIT',
    'media_type' => 'video/mp4',
    'media_category' => 'tweet_video',
    'total_bytes' => $size,
]);

Next, grab the media ID string from the result of this command and read the file in chunks, sending an APPEND command for each chunk. Here, we're sending in 4MB chunks - note that 5MB is the maximum Twitter accepts.

$mediaId = $media->media_id_string;
$segmentId = 0;
while (!feof($file)) {
    $chunk = fread($file, 4 * 1024 * 1024);

    $media = $twitter->media_upload([
        'command' => 'APPEND',
        'media_id' => $mediaId,
        'segment_index' => $segmentId,
        'media' => $chunk,
    ]);

    $segmentId++;
}

Once all chunks have been uploaded, close the handle to the file and send a FINALIZE command:

fclose($file);
$media = $twitter->media_upload([
    'command' => 'FINALIZE',
    'media_id' => $mediaId,
]);

Depending on the video you upload, Twitter may (or may not) choose to perform some additional processing on it. As a result, you need to check if it is being processed if so, wait until it is completed before you can successfully post a tweet.

if(isset($media->processing_info)) {
    $info = $media->processing_info;
    if($info->state != 'succeeded') {
        $attempts = 0;
        $checkAfterSecs = $info->check_after_secs;
        $success = false;
        do {
            $attempts++;
            sleep($checkAfterSecs);

            $media = $twitter->media_upload([
                'command' => 'STATUS',
                'media_id' => $mediaId,
            ]);

            $procInfo = $media->processing_info;

            if($procInfo->state == 'succeeded' || $procInfo->state == 'failed') {
                break;
            }

            $checkAfterSecs = $procInfo->check_after_secs;
        } while($attempts <= 10);
    }
}

At this point, you are ready to tweet!

$params = [
    'status' => 'This is the text of your tweet!',
    'media_ids' => $mediaId,
];

$tweet = $twitter->statuses_update($params);

Enjoy!