'English', 'es' => 'Spanish', 'it' => 'Italian', 'pt' => 'Portuguese', 'de' => 'German', 'fr' => 'French' ]; $beta_languages = [ 'pl' => 'Polish (Beta)', 'ru' => 'Russian (Beta)', 'nl' => 'Dutch (Beta)', 'ca' => 'Catalan (Beta)', 'tr' => 'Turkish (Beta)', 'sv' => 'Swedish (Beta)', 'uk' => 'Ukrainian (Beta)', 'no' => 'Norwegian (Beta)', 'fi' => 'Finnish (Beta)', 'sk' => 'Slovak (Beta)', 'el' => 'Greek (Beta)', 'cs' => 'Czech (Beta)', 'hr' => 'Croatian (Beta)', 'da' => 'Danish (Beta)', 'ro' => 'Romanian (Beta)', 'bg' => 'Bulgarian (Beta)' ]; return [ 'stable' => $stable_languages, 'beta' => $beta_languages ]; } /** * Get common languages for custom caption uploads * * @return array Array of language codes and names */ function muxvideo_get_custom_caption_languages() { return [ // Languages from stable list 'en' => __('English', '2coders-integration-mux-video'), 'es' => __('Spanish', '2coders-integration-mux-video'), 'fr' => __('French', '2coders-integration-mux-video'), 'de' => __('German', '2coders-integration-mux-video'), 'it' => __('Italian', '2coders-integration-mux-video'), 'pt' => __('Portuguese', '2coders-integration-mux-video'), 'nl' => __('Dutch', '2coders-integration-mux-video'), // Languages from beta list 'ja' => __('Japanese', '2coders-integration-mux-video'), 'zh' => __('Chinese', '2coders-integration-mux-video'), 'ko' => __('Korean', '2coders-integration-mux-video'), 'ru' => __('Russian', '2coders-integration-mux-video'), 'ar' => __('Arabic', '2coders-integration-mux-video'), 'hi' => __('Hindi', '2coders-integration-mux-video'), 'tr' => __('Turkish', '2coders-integration-mux-video'), 'pl' => __('Polish', '2coders-integration-mux-video'), 'sv' => __('Swedish', '2coders-integration-mux-video'), 'da' => __('Danish', '2coders-integration-mux-video'), 'fi' => __('Finnish', '2coders-integration-mux-video'), 'no' => __('Norwegian', '2coders-integration-mux-video'), 'hu' => __('Hungarian', '2coders-integration-mux-video'), 'sk' => __('Slovak', '2coders-integration-mux-video'), 'el' => __('Greek', '2coders-integration-mux-video'), 'cs' => __('Czech', '2coders-integration-mux-video'), 'hr' => __('Croatian', '2coders-integration-mux-video'), 'ro' => __('Romanian', '2coders-integration-mux-video'), 'bg' => __('Bulgarian', '2coders-integration-mux-video'), // Additional common languages 'af' => __('Afrikaans', '2coders-integration-mux-video'), 'sq' => __('Albanian', '2coders-integration-mux-video'), 'am' => __('Amharic', '2coders-integration-mux-video'), 'hy' => __('Armenian', '2coders-integration-mux-video'), 'az' => __('Azerbaijani', '2coders-integration-mux-video'), 'eu' => __('Basque', '2coders-integration-mux-video'), 'be' => __('Belarusian', '2coders-integration-mux-video'), 'bn' => __('Bengali', '2coders-integration-mux-video'), 'bs' => __('Bosnian', '2coders-integration-mux-video'), 'ca' => __('Catalan', '2coders-integration-mux-video'), 'ceb' => __('Cebuano', '2coders-integration-mux-video'), 'ny' => __('Chichewa', '2coders-integration-mux-video'), 'co' => __('Corsican', '2coders-integration-mux-video'), 'cy' => __('Welsh', '2coders-integration-mux-video'), 'et' => __('Estonian', '2coders-integration-mux-video'), 'tl' => __('Filipino', '2coders-integration-mux-video'), 'gl' => __('Galician', '2coders-integration-mux-video'), 'ka' => __('Georgian', '2coders-integration-mux-video'), 'gu' => __('Gujarati', '2coders-integration-mux-video'), 'ht' => __('Haitian Creole', '2coders-integration-mux-video'), 'ha' => __('Hausa', '2coders-integration-mux-video'), 'haw' => __('Hawaiian', '2coders-integration-mux-video'), 'he' => __('Hebrew', '2coders-integration-mux-video'), 'hmn' => __('Hmong', '2coders-integration-mux-video'), 'is' => __('Icelandic', '2coders-integration-mux-video'), 'ig' => __('Igbo', '2coders-integration-mux-video'), 'id' => __('Indonesian', '2coders-integration-mux-video'), 'ga' => __('Irish', '2coders-integration-mux-video'), 'jw' => __('Javanese', '2coders-integration-mux-video'), 'kn' => __('Kannada', '2coders-integration-mux-video'), 'kk' => __('Kazakh', '2coders-integration-mux-video'), 'km' => __('Khmer', '2coders-integration-mux-video'), 'ku' => __('Kurdish', '2coders-integration-mux-video'), 'ky' => __('Kyrgyz', '2coders-integration-mux-video'), 'lo' => __('Lao', '2coders-integration-mux-video'), 'la' => __('Latin', '2coders-integration-mux-video'), 'lv' => __('Latvian', '2coders-integration-mux-video'), 'lt' => __('Lithuanian', '2coders-integration-mux-video'), 'lb' => __('Luxembourgish', '2coders-integration-mux-video'), 'mk' => __('Macedonian', '2coders-integration-mux-video'), 'mg' => __('Malagasy', '2coders-integration-mux-video'), 'ms' => __('Malay', '2coders-integration-mux-video'), 'ml' => __('Malayalam', '2coders-integration-mux-video'), 'mt' => __('Maltese', '2coders-integration-mux-video'), 'mi' => __('Maori', '2coders-integration-mux-video'), 'mr' => __('Marathi', '2coders-integration-mux-video'), 'mn' => __('Mongolian', '2coders-integration-mux-video'), 'my' => __('Myanmar (Burmese)', '2coders-integration-mux-video'), 'ne' => __('Nepali', '2coders-integration-mux-video'), 'ps' => __('Pashto', '2coders-integration-mux-video'), 'fa' => __('Persian', '2coders-integration-mux-video'), 'pt-BR' => __('Portuguese (Brazil)', '2coders-integration-mux-video'), 'pt-PT' => __('Portuguese (Portugal)', '2coders-integration-mux-video'), 'pa' => __('Punjabi', '2coders-integration-mux-video'), 'si' => __('Sinhala', '2coders-integration-mux-video'), 'sl' => __('Slovenian', '2coders-integration-mux-video'), 'so' => __('Somali', '2coders-integration-mux-video'), 'es-ES' => __('Spanish (Spain)', '2coders-integration-mux-video'), 'es-MX' => __('Spanish (Mexico)', '2coders-integration-mux-video'), 'su' => __('Sundanese', '2coders-integration-mux-video'), 'sw' => __('Swahili', '2coders-integration-mux-video'), 'tg' => __('Tajik', '2coders-integration-mux-video'), 'ta' => __('Tamil', '2coders-integration-mux-video'), 'te' => __('Telugu', '2coders-integration-mux-video'), 'th' => __('Thai', '2coders-integration-mux-video'), 'uk' => __('Ukrainian', '2coders-integration-mux-video'), 'ur' => __('Urdu', '2coders-integration-mux-video'), 'uz' => __('Uzbek', '2coders-integration-mux-video'), 'vi' => __('Vietnamese', '2coders-integration-mux-video'), 'xh' => __('Xhosa', '2coders-integration-mux-video'), 'yi' => __('Yiddish', '2coders-integration-mux-video'), 'yo' => __('Yoruba', '2coders-integration-mux-video'), 'zu' => __('Zulu', '2coders-integration-mux-video') ]; } /** * Generate captions for a Mux asset * * @param string $asset_id The Mux asset ID * @param string $language_code The language code for captions * @return array Result with success status and message */ function muxvideo_generate_captions($asset_id, $language_code) { try { $assetsApi = muxvideo_api_client_init(); $asset = $assetsApi->getAsset($asset_id); $status = $asset->getData()->getStatus(); // Check asset status and return appropriate message if ($status === 'errored') { return [ 'success' => false, 'message' => 'Captions generation has failed. Try again or check asset settings.', 'message_type' => 'error' ]; } else if ($status === 'preparing') { return [ 'success' => true, 'message' => 'Captions generation is in progress. This may take a few minutes', 'message_type' => 'success' ]; } else if ($status !== 'ready') { return [ 'success' => false, 'message' => 'Asset is not ready yet. Current status: ' . $status, 'message_type' => 'error' ]; } $audio_track_id = null; $tracks = $asset->getData()->getTracks(); if ($tracks === null || empty($tracks)) { return [ 'success' => false, 'message' => 'Asset has no tracks. Cannot generate captions.' ]; } foreach ($tracks as $track) { if ($track->getType() === 'audio') { $audio_track_id = $track->getId(); break; } } if (!$audio_track_id) { return [ 'success' => false, 'message' => 'No audio track found for this asset. Cannot generate captions.' ]; } $languages = muxvideo_get_auto_caption_languages(); $all_languages = array_merge($languages['stable'], $languages['beta']); $language_name = isset($all_languages[$language_code]) ? $all_languages[$language_code] : $language_code; $subtitle = new \MuxPhp\Models\AssetGeneratedSubtitleSettings([ 'language_code' => $language_code, 'name' => $language_name . " (generated)" ]); $requestBody = new \MuxPhp\Models\GenerateTrackSubtitlesRequest([ 'generated_subtitles' => [$subtitle] ]); $assetsApi->generateAssetTrackSubtitles($asset_id, $audio_track_id, $requestBody); $message = __('captions_manager_status_in_progress', '2coders-integration-mux-video'); $message_type = 'success'; return [ 'success' => true, 'message' => $message, 'message_type' => $message_type, 'asset_id' => $asset_id, 'audio_track_id' => $audio_track_id, 'language_code' => $language_code ]; } catch (\MuxPhp\ApiException $e) { $responseBody = json_decode($e->getResponseBody(), true); // Check if this is the specific error about text_source if (isset($responseBody['error']['messages'][0]) && strpos($responseBody['error']['messages'][0], "Invalid value 'generated_vod' for 'text_source'") !== false) { return [ 'success' => false, 'message' => 'This asset already has auto-generated captions. Currently, Mux only supports one set of auto-generated captions per asset.' ]; } // For other errors, return the error message from the API if (isset($responseBody['error']['messages'][0])) { return [ 'success' => false, 'message' => 'API Error', 'message_type' => 'error' ]; } // Fallback error message return [ 'success' => false, 'message' => 'Error generating captions: ' . $e->getMessage(), 'message_type' => 'error' ]; } catch (Exception $e) { return [ 'success' => false, 'message' => 'Error: ' . $e->getMessage(), 'message_type' => 'error' ]; } } /** * Upload custom captions for a Mux asset * * @param string $asset_id The Mux asset ID * @param string $language_code The language code for the captions * @param string $file_url The URL of the temporary uploaded file * @param bool $closed_captions Whether these are closed captions * @return array Response with success status and message */ function muxvideo_upload_captions($asset_id, $language_code, $file_url, $closed_captions = false, $original_filename = '') { try { // Validate the asset ID if (empty($asset_id)) { return [ 'success' => false, 'message' => __('captions_manager_error_missing_asset', '2coders-integration-mux-video'), 'message_type' => 'error' ]; } // Validate the language code if (empty($language_code)) { return [ 'success' => false, 'message' => __('captions_manager_error_missing_language', '2coders-integration-mux-video'), 'message_type' => 'error' ]; } // Get file extension from original filename if (!empty($original_filename)) { $file_extension = strtolower(pathinfo($original_filename, PATHINFO_EXTENSION)); } else { $file_extension = strtolower(pathinfo($file_url, PATHINFO_EXTENSION)); } // Validate file type if (!in_array($file_extension, ['srt', 'vtt'])) { return [ 'success' => false, 'message' => __('captions_manager_error_format', '2coders-integration-mux-video'), 'message_type' => 'error' ]; } // Get language name $languages = muxvideo_get_custom_caption_languages(); $language_name = isset($languages[$language_code]) ? $languages[$language_code] : $language_code; // Initialize the Mux API client $assetsApi = muxvideo_api_client_init(); // Check if the asset exists and get its status $asset = $assetsApi->getAsset($asset_id); $status = $asset->getData()->getStatus(); // Check asset status and return appropriate message if ($status === 'errored') { return [ 'success' => false, 'message' => 'Captions upload has failed. Try again or check asset settings.', 'message_type' => 'error' ]; } else if ($status !== 'ready') { return [ 'success' => false, 'message' => 'Asset is not ready yet. Current status: ' . $status, 'message_type' => 'error' ]; } // Create the track using the Mux SDK $createTrackRequest = new \MuxPhp\Models\CreateTrackRequest([ 'url' => $file_url, 'type' => 'text', 'text_type' => 'subtitles', 'language_code' => $language_code, 'name' => $language_name, 'closed_captions' => $closed_captions ]); $track = $assetsApi->createAssetTrack($asset_id, $createTrackRequest); $track_data = $track->getData(); $track_status = $track_data->getStatus(); $response = [ 'success' => in_array($track_status, ['ready', 'preparing']), 'asset_id' => $asset_id, 'language_code' => $language_code, 'track_id' => $track_data->getId(), 'status' => $track_status ]; switch ($track_status) { case 'errored': $response['message'] = __('captions_manager_error_generation', '2coders-integration-mux-video'); $response['message_type'] = 'error'; break; case 'preparing': $response['message'] = __('captions_manager_status_preparing', '2coders-integration-mux-video'); $response['message_type'] = 'success'; break; case 'ready': $response['message'] = __('captions_manager_status_ready', '2coders-integration-mux-video'); $response['message_type'] = 'success'; break; default: $response['message'] = sprintf(__('captions_manager_status_unknown', '2coders-integration-mux-video'), $track_status); $response['message_type'] = 'error'; } return $response; } catch (Exception $e) { return [ 'success' => false, 'message' => sprintf(__('captions_manager_error_exception', '2coders-integration-mux-video'), $e->getMessage()), 'message_type' => 'error' ]; } } /** * Register the cron schedule for caption processing */ function muxvideo_register_caption_cron() { // Disabled as we now process captions directly in muxvideo_get_asset_captions wp_clear_scheduled_hook('muxvideo_process_caption_queue'); if (is_admin() && !wp_next_scheduled('muxvideo_process_caption_queue')) { wp_schedule_event(time(), 'every_10_seconds', 'muxvideo_process_caption_queue'); } } // Change from init to admin_init for better admin context handling remove_action('init', 'muxvideo_register_caption_cron'); add_action('admin_init', 'muxvideo_register_caption_cron'); /** * Add custom cron schedules */ function muxvideo_add_cron_schedules($schedules) { $schedules['every_10_seconds'] = array( 'interval' => 10, 'display' => __('Every 10 Seconds', '2coders-integration-mux-video') ); return $schedules; } add_filter('cron_schedules', 'muxvideo_add_cron_schedules'); /** * Process the caption queue */ function muxvideo_process_caption_queue() { global $wpdb; $table_name = $wpdb->prefix . 'mux_caption_queue'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange $pending_requests = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM `{$wpdb->prefix}mux_caption_queue` WHERE status = %s AND attempts < %d AND created_at >= DATE_SUB(NOW(), INTERVAL 1 DAY) ORDER BY created_at ASC LIMIT %d", 'pending', 50, 10 ) ); if (empty($pending_requests)) { return; } foreach ($pending_requests as $request) { // Update attempts count // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange $wpdb->update( $table_name, array( 'attempts' => $request->attempts + 1, 'updated_at' => current_time('mysql') ), array('id' => $request->id) ); // Get asset ID from upload ID if not already set $asset_id = $request->asset_id; if (empty($asset_id) && !empty($request->upload_id)) { try { $uploadsApi = muxvideo_api_client_init_upload(); $upload = $uploadsApi->getDirectUpload($request->upload_id); if ($upload && $upload->getData() && $upload->getData()->getAssetId()) { $asset_id = $upload->getData()->getAssetId(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange $wpdb->update( $table_name, array( 'asset_id' => $asset_id, 'updated_at' => current_time('mysql') ), array('id' => $request->id) ); } else { continue; } } catch (Exception $e) { if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- guarded by WP_DEBUG, intended for plugin debugging. error_log("Error retrieving asset ID"); } echo '
' . esc_html__('error_process_asset', '2coders-integration-mux-video') . '
'; continue; } } // Check if asset is ready before processing try { $assetsApi = muxvideo_api_client_init(); $asset = $assetsApi->getAsset($asset_id); $asset_data = $asset->getData(); if ($asset_data->getStatus() !== 'ready') { // Asset not ready, skip processing but keep in queue // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange $wpdb->update( $table_name, array( 'updated_at' => current_time('mysql'), 'error_message' => 'Asset is not ready yet' ), array('id' => $request->id) ); continue; } } catch (Exception $e) { if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- guarded by WP_DEBUG, intended for plugin debugging. error_log("Error checking asset status"); } echo '
' . esc_html__('error_process_status', '2coders-integration-mux-video') . '
'; continue; } $original_filename = isset($request->original_filename) ? $request->original_filename : ''; // Process based on caption type only if asset is ready if ($request->caption_type === 'auto') { $result = muxvideo_generate_captions($asset_id, $request->language_code); if ($result['success']) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange $wpdb->update( $table_name, array( 'status' => 'completed', 'updated_at' => current_time('mysql') ), array('id' => $request->id) ); } else { $is_permanent_error = strpos($result['message'], 'already has auto-generated captions') !== false; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange $wpdb->update( $table_name, array( 'status' => $is_permanent_error ? 'failed' : 'pending', 'error_message' => $result['message'], 'updated_at' => current_time('mysql') ), array('id' => $request->id) ); } } else if ($request->caption_type === 'custom') { if (empty($request->caption_file_url)) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange $wpdb->update( $table_name, array( 'status' => 'failed', 'error_message' => 'No caption file URL provided for custom caption', 'updated_at' => current_time('mysql') ), array('id' => $request->id) ); continue; } $result = muxvideo_upload_captions( $asset_id, $request->language_code, $request->caption_file_url, (bool)$request->closed_captions, $original_filename ); if ($result['success']) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange $wpdb->update( $table_name, array( 'status' => 'completed', 'updated_at' => current_time('mysql') ), array('id' => $request->id) ); } else { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange $wpdb->update( $table_name, array( 'status' => 'pending', 'error_message' => $result['message'], 'updated_at' => current_time('mysql') ), array('id' => $request->id) ); } } // Avoid rate limits sleep(1); } } add_action('muxvideo_process_caption_queue', 'muxvideo_process_caption_queue'); /** * Save caption request to database */ function muxvideo_save_caption_request_handler($upload_id, $caption_type, $language_code, $closed_captions) { $upload_id = isset($upload_id) ? sanitize_text_field($upload_id) : ''; $caption_type = isset($caption_type) ? sanitize_text_field($caption_type) : ''; $language_code = isset($language_code) ? sanitize_text_field($language_code) : ''; if (empty($upload_id) || empty($caption_type) || empty($language_code)) { return; } $closed_captions = isset($closed_captions) && $closed_captions == 1; global $wpdb; $table_name = $wpdb->prefix . 'mux_caption_queue'; $data = array( 'upload_id' => $upload_id, 'caption_type' => $caption_type, 'language_code' => $language_code, 'closed_captions' => $closed_captions ? 1 : 0, 'status' => 'pending', 'created_at' => current_time('mysql'), 'updated_at' => current_time('mysql') ); // phpcs:disable WordPress.Security.NonceVerification.Missing -- caller (muxvideo_reset_upload_url) verifies the nonce before invoking this function. if ( 'custom' === $caption_type && isset( $_FILES['caption_file'] ) && isset( $_FILES['caption_file']['error'] ) && UPLOAD_ERR_OK === (int) $_FILES['caption_file']['error'] ) { $file_name = isset( $_FILES['caption_file']['name'] ) ? sanitize_file_name( wp_unslash( $_FILES['caption_file']['name'] ) ) : ''; $file_extension = strtolower( pathinfo( $file_name, PATHINFO_EXTENSION ) ); if ( ! in_array( $file_extension, array( 'srt', 'vtt' ), true ) ) { return; } if ( ! function_exists( 'wp_handle_upload' ) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; } $upload = wp_handle_upload( $_FILES['caption_file'], // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- handled by wp_handle_upload array( 'test_form' => false ) ); if ( ! is_array( $upload ) || isset( $upload['error'] ) ) { return; } $upload_dir = wp_upload_dir(); $upload_id_sn = preg_replace( '/[^a-zA-Z0-9_-]/', '', $upload_id ); $caption_dir = $upload_dir['basedir'] . '/mux-captions/' . $upload_id_sn; if ( ! file_exists( $caption_dir ) ) { wp_mkdir_p( $caption_dir ); } global $wp_filesystem; if ( empty( $wp_filesystem ) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; WP_Filesystem(); } $unique_filename = $language_code . '.' . $file_extension; $file_path = $caption_dir . '/' . $unique_filename; if ( $wp_filesystem && $wp_filesystem->copy( $upload['file'], $file_path, true ) ) { $wp_filesystem->delete( $upload['file'] ); $data['caption_file_path'] = $file_path; $data['caption_file_url'] = $upload_dir['baseurl'] . '/mux-captions/' . $upload_id_sn . '/' . $unique_filename; } else { return; } } // phpcs:enable WordPress.Security.NonceVerification.Missing // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange $wpdb->insert( $table_name, $data ); }