Skip to content

Instantly share code, notes, and snippets.

@mizner
Created May 31, 2025 14:21
Show Gist options
  • Save mizner/8e9c5fe56e40c6f1f7c887c352a7875b to your computer and use it in GitHub Desktop.
Save mizner/8e9c5fe56e40c6f1f7c887c352a7875b to your computer and use it in GitHub Desktop.
WP FFMPEG Transcoder .mov to .mp4
<?php
/**
* Plugin Name: Custom Transcoder (WP Cron + Attachment Update + Email + UI Button)
*/
// 1. Schedule transcoding on MOV upload
add_filter('wp_handle_upload', function($upload) {
$ext = strtolower(wp_check_filetype($upload['file'])['ext']);
if ($ext !== 'mov') return $upload;
wp_schedule_single_event(time() + 10, 'custom_transcoder_convert_mov', [$upload]);
return $upload;
});
// 2. Cron job: Convert MOV to MP4 with improved FFmpeg args
add_action('custom_transcoder_convert_mov', function($upload) {
if (empty($upload['file']) || !file_exists($upload['file'])) {
error_log('[Transcoder] File not found');
custom_transcoder_send_email('fail', $upload['file'], 'File not found.');
return;
}
$original = $upload['file'];
$path = pathinfo($original);
$mp4 = $path['dirname'] . '/' . $path['filename'] . '.mp4';
$cmd = "ffmpeg -y -i " . escapeshellarg($original) .
" -map 0:v:0 -map 0:a:0 -c copy -movflags +faststart " .
escapeshellarg($mp4) . " 2>&1";
error_log('[Transcoder] Running: ' . $cmd);
exec($cmd, $out, $code);
if ($code !== 0 || !file_exists($mp4) || filesize($mp4) < 1000) {
error_log('[Transcoder] FFmpeg failed: ' . implode("\n", $out));
custom_transcoder_send_email('fail', $original, implode("\n", $out));
return;
}
global $wpdb;
// Update media library
$attachment_id = attachment_url_to_postid($upload['url']);
if ($attachment_id) {
update_attached_file($attachment_id, $mp4);
// ✅ Ensure required media functions are loaded
if (!function_exists('wp_generate_attachment_metadata')) {
require_once ABSPATH . 'wp-admin/includes/image.php';
}
if (!function_exists('wp_read_video_metadata')) {
require_once ABSPATH . 'wp-admin/includes/media.php';
}
wp_update_post([
'ID' => $attachment_id,
'post_mime_type' => 'video/mp4',
]);
$meta = wp_generate_attachment_metadata($attachment_id, $mp4);
if (!empty($meta)) wp_update_attachment_metadata($attachment_id, $meta);
// ✅ Also update GUID directly to .mp4
$mp4_url = str_replace('.mov', '.mp4', $upload['url']);
$wpdb->update(
$wpdb->posts,
['guid' => $mp4_url],
['ID' => $attachment_id]
);
error_log("[Transcoder] Updated wp_posts.guid for attachment ID $attachment_id");
}
// ✅ Update rtMedia <video src="..."> in activity feed
$mov_basename = basename($upload['url']);
$mp4_basename = basename($mp4);
$like = '%' . $mov_basename . '%';
$activities = $wpdb->get_results($wpdb->prepare(
"SELECT id, content FROM {$wpdb->prefix}bp_activity WHERE type = 'rtmedia_update' AND content LIKE %s",
$like
));
foreach ($activities as $activity) {
$new_content = str_replace($mov_basename, $mp4_basename, $activity->content);
$wpdb->update(
"{$wpdb->prefix}bp_activity",
['content' => $new_content],
['id' => $activity->id]
);
error_log("[Transcoder] Updated bp_activity ID {$activity->id} video src to .mp4");
}
unlink($original);
error_log('[Transcoder] Success: ' . $mp4);
custom_transcoder_send_email('success', $mp4);
});
// 3. Update metadata post-transcode for existing attachments (fallback only)
add_action('add_attachment', function($post_ID) {
$file = get_attached_file($post_ID);
if (!$file || strtolower(pathinfo($file, PATHINFO_EXTENSION)) !== 'mov') return;
$mp4 = preg_replace('/\.mov$/i', '.mp4', $file);
if (!file_exists($mp4)) return;
update_attached_file($post_ID, $mp4);
if (!function_exists('wp_generate_attachment_metadata')) {
require_once ABSPATH . 'wp-admin/includes/image.php';
}
if (!function_exists('wp_read_video_metadata')) {
require_once ABSPATH . 'wp-admin/includes/media.php';
}
wp_update_post([
'ID' => $post_ID,
'post_mime_type' => 'video/mp4',
]);
$meta = wp_generate_attachment_metadata($post_ID, $mp4);
if (!empty($meta)) wp_update_attachment_metadata($post_ID, $meta);
// Also update guid
global $wpdb;
$wpdb->update(
$wpdb->posts,
['guid' => str_replace('.mov', '.mp4', wp_get_attachment_url($post_ID))],
['ID' => $post_ID]
);
});
// 4. Email notification
function custom_transcoder_send_email($status, $file, $details = '') {
$subj = $status === 'success' ? '✅ Video Transcoded' : '❌ Transcoding Failed';
$body = "File: $file\n\n" . ($status === 'success' ? "Transcoding completed successfully." : "Transcoding failed.\n\nDetails:\n$details");
wp_mail('[email protected]', $subj, $body);
}
// 5. "Transcode Now" button in media editor
add_filter('attachment_fields_to_edit', function($fields, $post) {
$file = get_attached_file($post->ID);
if (!$file || strtolower(pathinfo($file, PATHINFO_EXTENSION)) !== 'mov') return $fields;
$nonce = wp_create_nonce('retranscode_' . $post->ID);
$fields['retranscode'] = [
'label' => 'Custom Transcoder',
'input' => 'html',
'html' => '<button type="button" class="button retranscode-mov-button" data-attachment="' . esc_attr($post->ID) . '" data-nonce="' . esc_attr($nonce) . '">Transcode Now</button>' .
'<span class="retranscode-status" style="margin-left:10px;"></span>',
];
return $fields;
}, 10, 2);
// 6. Handle AJAX trigger
add_action('wp_ajax_custom_transcoder_retranscode', function() {
if (!current_user_can('upload_files')) wp_send_json_error(['message' => 'Permission denied.']);
$id = intval($_POST['attachment_id'] ?? 0);
$nonce = $_POST['nonce'] ?? '';
if (!wp_verify_nonce($nonce, 'retranscode_' . $id)) wp_send_json_error(['message' => 'Invalid nonce.']);
if (!get_post($id)) wp_send_json_error(['message' => 'Attachment not found.']);
$file = get_attached_file($id);
if (!$file || strtolower(pathinfo($file, PATHINFO_EXTENSION)) !== 'mov') {
wp_send_json_error(['message' => 'Not a .mov file or file not found.']);
}
wp_schedule_single_event(time() + 5, 'custom_transcoder_convert_mov', [[
'file' => $file,
'type' => get_post_mime_type($id),
'url' => wp_get_attachment_url($id),
]]);
wp_send_json_success(['message' => 'Transcoding job scheduled.']);
});
// 7. Inject JS into media screen
add_action('admin_footer', function() {
$screen = get_current_screen();
if (!$screen || !in_array($screen->id, ['upload', 'media'])) return;
?>
<script>
jQuery(document).on('click', '.retranscode-mov-button', function () {
const $btn = jQuery(this);
const id = $btn.data('attachment');
const nonce = $btn.data('nonce');
const $status = $btn.siblings('.retranscode-status');
$status.text('Scheduling...');
jQuery.post(ajaxurl, {
action: 'custom_transcoder_retranscode',
attachment_id: id,
nonce: nonce,
}).done(r => {
$status.text(r.data.message || 'Scheduled.');
}).fail(xhr => {
const msg = xhr.responseJSON?.data?.message || 'Error.';
$status.text('Failed: ' + msg);
});
});
</script>
<?php
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment