Technical Solution Verified Solution

how to upload a video from inside moodle offical app using a custom plugin an...

R

Al-Rashid AI

Apr 19, 2026

Problem Summary

"Hi all,I just want to share this since I used the form knowledge to achieve it.Apparently it's a real struggle to use the build this in a custom plugin.So here it is FULLY working and tested.local/videoupload/templates/mobile_main.mustache<ion-list><ion-item lines="none"><ion-label><h2>Videoupload</h2><p>Select 1 video and send it.</p></ion-label></ion-item><core-attachments[files]="attachments"[maxSize]="CONTENT_OTHERDATA.maxsize"[maxSubmissions]="1"[component]="'local_videoupload'"[acceptedTypes]="CONTENT_OTHERDATA.filetypes"[allowOffline]="false"></core-attachments><ion-buttonexpand="block"color="primary"(click)="sendVideo()"name="local_videoupload_send">Send</ion-button></ion-list>local/videoupload/db/mobile.php<?phpdefined('MOODLE_INTERNAL') || die();$addons = ['local_videoupload' => ['handlers' => ['videoupload_menu' => ['delegate'    => 'CoreMainMenuDelegate','method'      => 'mobile_view','displaydata' => ['title' => 'pluginname','icon'  => 'fa-video',],'priority'    => 800,],],'lang' => [['pluginname', 'local_videoupload'],['send', 'local_videoupload'],['novideo', 'local_videoupload'],['uploadok', 'local_videoupload'],['uploadfail', 'local_videoupload'],],],];local/videoupload/classes/external.php<?phpnamespace local_videoupload;use external_api;use external_function_parameters;use external_single_structure;use external_value;defined('MOODLE_INTERNAL') || die();require_once($CFG->libdir . '/externallib.php');class external extends external_api {public static function save_parameters() {return new external_function_parameters(['draftitemid' => new external_value(PARAM_INT, 'Draft itemid from mobile upload'),]);}public static function save($draftitemid) {global $USER, $CFG;$params = self::validate_parameters(self::save_parameters(),['draftitemid' => $draftitemid]);$usercontext = \context_user::instance($USER->id);self::validate_context($usercontext);// Ensure file API functions are available.require_once($CFG->libdir . '/filelib.php');$finalitemid = time();\file_save_draft_area_files($params['draftitemid'],$usercontext->id,'local_videoupload','video',$finalitemid,['subdirs' => 0, 'maxfiles' => 1]);return ['status' => true,'itemid' => $finalitemid,];}public static function save_returns() {return new external_single_structure(['status' => new external_value(PARAM_BOOL, 'Success'),'itemid'  => new external_value(PARAM_INT, 'Stored itemid in plugin filearea'),]);}}ocal/videoupload/mobile/js/template_javascript.js/*** Moodle App site plugin JS.* Important: core-attachments expects to mutate a top-level array bound via [files].* So we use this.attachments (NOT CONTENT_OTHERDATA.attachments).*/// The array that <core-attachments> will update.this.attachments = [];this.sendVideo = async function () {const attachments = this.attachments || [];if (!attachments.length) {this.CoreDomUtilsProvider.showToast(this.TranslateService.instant('local_videoupload.novideo'),true,4000);return false;}const modal = await this.CoreDomUtilsProvider.showModalLoading('core.sending', true);try {const uploadResult = await this.CoreFileUploaderProvider.uploadOrReuploadFiles(attachments);// Handle versions that return itemid as a number/string directly.let draftItemId = null;if (typeof uploadResult === 'number') {draftItemId = uploadResult;} else if (typeof uploadResult === 'string' && uploadResult.trim() !== '' && !isNaN(parseInt(uploadResult, 10))) {draftItemId = parseInt(uploadResult, 10);} else {draftItemId =(uploadResult && uploadResult.itemid) ||(uploadResult && uploadResult.draftitemid) ||(uploadResult && uploadResult.draftid) ||(uploadResult && uploadResult.data && uploadResult.data.itemid) ||(uploadResult && uploadResult.result && uploadResult.result.itemid) ||(uploadResult && uploadResult.filesresult && uploadResult.filesresult.itemid) ||(uploadResult && uploadResult[0] && (uploadResult[0].itemid || uploadResult[0].draftitemid));if (!draftItemId && attachments[0]) {draftItemId =attachments[0].itemid ||attachments[0].draftitemid ||attachments[0].draftid;}}if (!draftItemId) {throw new Error('Upload finished but draft itemid was not returned.');}const site = await this.CoreSitesProvider.getSite();const result = await site.write('local_videoupload_save', { draftitemid: draftItemId });if (result && result.status) {this.CoreDomUtilsProvider.showToast(this.TranslateService.instant('local_videoupload.uploadok'),true,6000);} else {this.CoreDomUtilsProvider.showToast(this.TranslateService.instant('local_videoupload.uploadfail'),true,6000);}return result;} catch (error) {this.CoreDomUtilsProvider.showErrorModalDefault(error, 'local_videoupload.uploadfail');return false;} finally {modal.dismiss();try { this.CoreFileUploaderProvider.clearTmpFiles(attachments); } catch (e) {}this.attachments = [];}};local/videoupload/classes/output/mobile.php<?phpnamespace local_videoupload\output;defined('MOODLE_INTERNAL') || die();class mobile {public static function mobile_view($args) {global $CFG, $OUTPUT;$maxbytes = isset($CFG->maxbytes) ? (int) $CFG->maxbytes : 0;// IMPORTANT: Keep otherdata flat + scalar strings (most compatible).$otherdata = ['maxsize'     => (string) $maxbytes,'filetypes'   => '.mp4,.mov,.m4v,.webm','attachments' => '', // empty string, JS will turn it into []];return ['templates' => [['id' => 'main','html' => $OUTPUT->render_from_template('local_videoupload/mobile_main', []),],],'javascript' => file_get_contents($CFG->dirroot. '/local/videoupload/mobile/js/template_javascript.js'),'otherdata' => $otherdata,];}}local/videoupload/db/services.php<?phpdefined('MOODLE_INTERNAL') || die();$functions = ['local_videoupload_save' => ['classname'   => 'local_videoupload\external','methodname'  => 'save','description' => 'Finalize a draft video upload.','type'        => 'write','services'    => [MOODLE_OFFICIAL_MOBILE_SERVICE],],];local/videoupload/version.php<?phpdefined('MOODLE_INTERNAL') || die();$plugin->component = 'local_videoupload';$plugin->version   = 2025122906;$plugin->requires  = 2022041900; // Moodle 4.0+ (adjust if needed). --- ### 🤖 الحل الهندسي المقترح: This Standard Operating Procedure (SOP) details how to implement and use a custom Moodle local plugin (`local_videoupload`) to enable video uploads directly from the Moodle official mobile app. The solution leverages Moodle's mobile API, file picker component (`core-attachments`), and external services to handle the upload process efficiently. --- ## Standard Operating Procedure: How to Upload a Video from Inside Moodle Official App Using a Custom Plugin and File Picker ### 1. Introduction and Overview This document provides a step-by-step guide for deploying and utilizing a custom Moodle local plugin, `local_videoupload`, designed to facilitate video uploads from the Moodle official mobile application. The plugin integrates with the mobile app's native file picker via the `core-attachments` component, uploads the selected video to a temporary (draft) area, and then moves it to a permanent file area associated with the plugin. This approach is robust and leverages Moodle's core file handling capabilities. ### 2. Prerequisites * A running Moodle instance (Moodle 4.0 or newer is recommended based on the plugin's `requires` version `2022041900`). * Administrator access to the Moodle site. * Command-line (SSH) access to the Moodle server for plugin installation. * The Moodle Official App installed on a mobile device (Android/iOS). * Basic understanding of Moodle plugin structure and Linux filesystem navigation. ### 3. Plugin Files and Structure The custom plugin `local_videoupload` consists of the following files and directories. You will need to create this exact structure within your Moodle installation's `local/` directory. ``` moodle_root/ ├── local/ │ └── videoupload/ │ ├── classes/ │ │ ├── external.php │ │ └── output/ │ │ └── mobile.php │ ├── db/ │ │ ├── mobile.php │ │ └── services.php │ ├── lang/ │ │ └── en/ │ │ └── local_videoupload.php (This file is required, even if empty initially, or populated with strings) │ ├── mobile/ │ │ └── js/ │ │ └── template_javascript.js │ ├── templates/ │ │ └── mobile_main.mustache │ └── version.php ``` ### 4. Installation Steps #### 4.1. Create Plugin Directory and Files 1. **Connect to your Moodle server:** Use SSH to connect to your Moodle server. ```bash ssh your_user@your_moodle_server_ip ``` 2. **Navigate to Moodle's `local` directory:** ```bash cd /path/to/your/moodle/local ``` *(Replace `/path/to/your/moodle` with the actual path to your Moodle installation directory, e.g., `/var/www/html/moodle`)*. 3. **Create the plugin directory:** ```bash mkdir videoupload cd videoupload ``` 4. **Create subdirectories:** ```bash mkdir -p classes/output db lang/en mobile/js templates ``` 5. **Create the required language file:** Even if empty, this file must exist for the `lang` entries in `db/mobile.php` to resolve correctly without warnings. ```bash touch lang/en/local_videoupload.php ``` *(You can add the language strings later, e.g., `$string['pluginname'] = 'Video Upload';` etc.)* 6. **Populate files with provided code:** For each file listed below, create it in the specified path and paste the corresponding code from the problem description. * `local/videoupload/templates/mobile_main.mustache` ```html <ion-list> <ion-item lines="none"> <ion-label> <h2>Videoupload</h2> <p>Select 1 video and send it.</p> </ion-label> </ion-item> <core-attachments [files]="attachments" [maxSize]="CONTENT_OTHERDATA.maxsize" [maxSubmissions]="1" [component]="'local_videoupload'" [acceptedTypes]="CONTENT_OTHERDATA.filetypes" [allowOffline]="false" ></core-attachments> <ion-button expand="block" color="primary" (click)="sendVideo()" name="local_videoupload_send">Send</ion-button> </ion-list> ``` * `local/videoupload/db/mobile.php` ```php <?php defined('MOODLE_INTERNAL') || die(); $addons = [ 'local_videoupload' => [ 'handlers' => [ 'videoupload_menu' => [ 'delegate' => 'CoreMainMenuDelegate', 'method' => 'mobile_view', 'displaydata' => [ 'title' => 'pluginname', 'icon' => 'fa-video', ], 'priority' => 800, ], ], 'lang' => [ ['pluginname', 'local_videoupload'], ['send', 'local_videoupload'], ['novideo', 'local_videoupload'], ['uploadok', 'local_videoupload'], ['uploadfail', 'local_videoupload'], ], ], ]; ``` * `local/videoupload/classes/external.php` ```php <?php namespace local_videoupload; use external_api; use external_function_parameters; use external_single_structure; use external_value; defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir . '/externallib.php'); class external extends external_api { public static function save_parameters() { return new external_function_parameters([ 'draftitemid' => new external_value(PARAM_INT, 'Draft itemid from mobile upload'), ]); } public static function save($draftitemid) { global $USER, $CFG; $params = self::validate_parameters(self::save_parameters(), [ 'draftitemid' => $draftitemid ]); $usercontext = \context_user::instance($USER->id); self::validate_context($usercontext); // Ensure file API functions are available. require_once($CFG->libdir . '/filelib.php'); $finalitemid = time(); // Use current timestamp as a unique itemid for this upload. \file_save_draft_area_files( $params['draftitemid'], $usercontext->id, 'local_videoupload', 'video', $finalitemid, ['subdirs' => 0, 'maxfiles' => 1] ); return [ 'status' => true, 'itemid' => $finalitemid, ]; } public static function save_returns() { return new external_single_structure([ 'status' => new external_value(PARAM_BOOL, 'Success'), 'itemid' => new external_value(PARAM_INT, 'Stored itemid in plugin filearea'), ]); } } ``` * `local/videoupload/mobile/js/template_javascript.js` ```javascript /*** Moodle App site plugin JS. * Important: core-attachments expects to mutate a top-level array bound via [files]. * So we use this.attachments (NOT CONTENT_OTHERDATA.attachments). */ // The array that <core-attachments> will update. this.attachments = []; this.sendVideo = async function () { const attachments = this.attachments || []; if (!attachments.length) { this.CoreDomUtilsProvider.showToast(this.TranslateService.instant('local_videoupload.novideo'), true, 4000); return false; } const modal = await this.CoreDomUtilsProvider.showModalLoading('core.sending', true); try { const uploadResult = await this.CoreFileUploaderProvider.uploadOrReuploadFiles(attachments); // Handle versions that return itemid as a number/string directly. let draftItemId = null; if (typeof uploadResult === 'number') { draftItemId = uploadResult; } else if (typeof uploadResult === 'string' && uploadResult.trim() !== '' && !isNaN(parseInt(uploadResult, 10))) { draftItemId = parseInt(uploadResult, 10); } else { draftItemId = (uploadResult && uploadResult.itemid) || (uploadResult && uploadResult.draftitemid) || (uploadResult && uploadResult.draftid) || (uploadResult && uploadResult.data && uploadResult.data.itemid) || (uploadResult && uploadResult.result && uploadResult.result.itemid) || (uploadResult && uploadResult.filesresult && uploadResult.filesresult.itemid) || (uploadResult && uploadResult[0] && (uploadResult[0].itemid || uploadResult[0].draftitemid)); if (!draftItemId && attachments[0]) { draftItemId = attachments[0].itemid || attachments[0].draftitemid || attachments[0].draftid; } } if (!draftItemId) { throw new Error('Upload finished but draft itemid was not returned.'); } const site = await this.CoreSitesProvider.getSite(); const result = await site.write('local_videoupload_save', { draftitemid: draftItemId }); if (result && result.status) { this.CoreDomUtilsProvider.showToast(this.TranslateService.instant('local_videoupload.uploadok'), true, 6000); } else { this.CoreDomUtilsProvider.showToast(this.TranslateService.instant('local_videoupload.uploadfail'), true, 6000); } return result; } catch (error) { this.CoreDomUtilsProvider.showErrorModalDefault(error, 'local_videoupload.uploadfail'); return false; } finally { modal.dismiss(); try { this.CoreFileUploaderProvider.clearTmpFiles(attachments); } catch (e) {} this.attachments = []; } }; ``` * `local/videoupload/classes/output/mobile.php` ```php <?php namespace local_videoupload\output; defined('MOODLE_INTERNAL') || die(); class mobile { public static function mobile_view($args) { global $CFG, $OUTPUT; $maxbytes = isset($CFG->maxbytes) ? (int) $CFG->maxbytes : 0; // IMPORTANT: Keep otherdata flat + scalar strings (most compatible). $otherdata = [ 'maxsize' => (string) $maxbytes, 'filetypes' => '.mp4,.mov,.m4v,.webm', 'attachments' => '', // empty string, JS will turn it into [] ]; return [ 'templates' => [ [ 'id' => 'main', 'html' => $OUTPUT->render_from_template('local_videoupload/mobile_main', []), ], ], 'javascript' => file_get_contents($CFG->dirroot . '/local/videoupload/mobile/js/template_javascript.js'), 'otherdata' => $otherdata, ]; } } ``` * `local/videoupload/db/services.php` ```php <?php defined('MOODLE_INTERNAL') || die(); $functions = [ 'local_videoupload_save' => [ 'classname' => 'local_videoupload\external', 'methodname' => 'save', 'description' => 'Finalize a draft video upload.', 'type' => 'write', 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], ], ]; ``` * `local/videoupload/version.php` ```php <?php defined('MOODLE_INTERNAL') || die(); $plugin->component = 'local_videoupload'; $plugin->version = 2025122906; $plugin->requires = 2022041900; // Moodle 4.0+ (adjust if needed). ``` #### 4.2. Run Moodle Upgrade 1. **Access Moodle Administration:** Open your web browser and navigate to your Moodle site's administration page (e.g., `http://your_moodle_site/admin`). 2. **Complete Installation:** Moodle will detect the new plugin and prompt you to upgrade. Follow the on-screen instructions to complete the installation. 3. **Verify Plugin:** Once the upgrade is complete, you can navigate to `Site administration > Plugins > Plugins overview` and search for "Video Upload" (or `local_videoupload`). Ensure it is listed and enabled. ### 5. Code Explanation and Functionality Breakdown This section explains how each part of the plugin contributes to the video upload functionality. * **`local/videoupload/version.php`**: * Standard Moodle plugin metadata. It declares the plugin's component name (`local_videoupload`), its version (`2025122906`), and the minimum Moodle version it requires (`2022041900` for Moodle 4.0). * **`local/videoupload/db/mobile.php`**: * This file registers the plugin's mobile capabilities. * It defines a `videoupload_menu` handler that integrates the plugin into the Moodle App's main menu. * `delegate => 'CoreMainMenuDelegate'` ensures it appears in the main menu. * `method => 'mobile_view'` tells the Moodle App to call the `mobile_view` function in the `local_videoupload\output\mobile` class to render the page. * `displaydata` sets the title (using `pluginname` language string) and an icon (`fa-video`). * The `lang` array lists all language strings used by the mobile plugin, ensuring they are downloaded to the app for offline use and translation. * **`local/videoupload/db/services.php`**: * This file defines the Moodle Web Services function `local_videoupload_save`. * It links this service name to the `save` method within the `local_videoupload\external` class. * `type => 'write'` indicates that this service modifies data. * `services => [MOODLE_OFFICIAL_MOBILE_SERVICE]` restricts this service to only be callable by the official Moodle Mobile App. * **`local/videoupload/classes/external.php`**: * This class defines the backend logic for finalizing the video upload. * `save_parameters()`: Declares that the `save` function expects a single parameter, `draftitemid`, which is an integer representing the ID of the file(s) in Moodle's temporary file area. * `save($draftitemid)`: * Validates the incoming `draftitemid`. * Retrieves the current user's context. * It uses `file_save_draft_area_files` to move the files from the temporary draft area to a permanent file area. * `$params['draftitemid']`: The ID of the files in the draft area. * `$usercontext->id`: The context ID where the files will be stored (here, the user context). * `'local_videoupload'`: The component name associated with these files. * `'video'`: The file area name within the component. * `$finalitemid`: A unique identifier (timestamp in this case) for this specific file group. * `['subdirs' => 0, 'maxfiles' => 1]`: Ensures no subdirectories are created and only one file is allowed. * Returns a status and the final `itemid` (which is the `$finalitemid` used above). * **`local/videoupload/classes/output/mobile.php`**: * This class is responsible for generating the content for the mobile view (`mobile_view` method, as defined in `db/mobile.php`). * It retrieves Moodle's `maxbytes` setting to pass to the mobile app. * `$otherdata`: This array is crucial for passing configuration to the mobile app's JavaScript and template. * `maxsize`: The maximum file size allowed, derived from Moodle's `maxbytes`. * `filetypes`: Hardcoded list of accepted video file extensions. * `attachments`: An empty string, which the JavaScript will interpret as an empty array for `core-attachments`. * It renders the `mobile_main.mustache` template. * It injects the JavaScript from `template_javascript.js` directly into the mobile page. * **`local/videoupload/templates/mobile_main.mustache`**: * This is the Mustache template that defines the user interface within the Moodle mobile app. * `<core-attachments>`: This is a Moodle mobile app component that provides a standardized file picker interface. * `[files]="attachments"`: Binds the component to the `attachments` array in the JavaScript, which holds the selected files. * `[maxSize]="CONTENT_OTHERDATA.maxsize"`: Passes the maximum file size limit from the PHP output. * `[maxSubmissions]="1"`: Allows only one file to be selected. * `[component]="'local_videoupload'"`: Associates the upload with this plugin's component. * `[acceptedTypes]="CONTENT_OTHERDATA.filetypes"`: Specifies allowed file types. * `<ion-button (click)="sendVideo()">`: A button that, when clicked, calls the `sendVideo` function defined in the JavaScript. * **`local/videoupload/mobile/js/template_javascript.js`**: * This is the client-side logic for the mobile app page. * `this.attachments = []`: Initializes the array that `core-attachments` will populate with selected files. * `this.sendVideo = async function ()`: The asynchronous function triggered by the "Send" button. * **Validation**: Checks if any files have been selected. If not, shows a toast message. * **Loading Modal**: Displays a "Sending..." modal during the upload process. * **`CoreFileUploaderProvider.uploadOrReuploadFiles(attachments)`**: This is the core mobile API call to upload the selected files to Moodle's temporary (draft) file area. It returns a `draftitemid`. * **`draftItemId` extraction**: The code includes robust logic to extract the `draftitemid` from various possible return formats of `uploadOrReuploadFiles`, making it compatible with different Moodle App versions or edge cases. * **`site.write('local_videoupload_save', { draftitemid: draftItemId })`**: This is where the Moodle App calls the custom external function `local_videoupload_save` (defined in `db/services.php` and implemented in `classes/external.php`) to finalize the upload by moving the files from the draft area to their permanent location. * **Feedback**: Shows success or failure toast messages based on the result of the `site.write` call. * **Cleanup**: * `modal.dismiss()`: Hides the loading modal. * `CoreFileUploaderProvider.clearTmpFiles(attachments)`: Clears temporary files from the mobile app's cache. * `this.attachments = []`: Resets the attachments array to clear the file picker. ### 6. Usage from Moodle Mobile App 1. **Open Moodle Official App:** Launch the Moodle App on your mobile device. 2. **Connect to your Moodle site:** Ensure you are logged into your Moodle site where the plugin was installed. 3. **Navigate to the Main Menu:** Access the main menu (usually a hamburger icon `☰`). 4. **Find "Videoupload":** Look for an entry titled "Videoupload" (or the translated version if you added language strings) with a video camera icon (🎬 or similar). Tap on it. 5. **Select a Video:** * The "Videoupload" page will open, displaying "Select 1 video and send it." * Tap on the file picker area (it usually shows "Add file..." or a plus icon). * Your device's file picker or gallery will open. Select a single video file (e.g., MP4, MOV). 6. **Send the Video:** * Once the video is selected, its name will appear in the attachments area. * Tap the "Send" button. 7. **Monitor Upload Progress:** * A "Sending..." loading modal will appear. * Upon completion, a toast message will confirm "Upload OK" or "Upload Failed." * The file picker will clear, indicating the process is complete. ### 7. Verification After a successful upload: 1. **Check Moodle Server Files:** * Connect to your Moodle server via SSH. * Navigate to your Moodle data directory (e.g., `/path/to/your/moodledata`). * Look for files stored by the plugin. They should be in a path similar to: `filedir/U/S/USERID/local_videoupload/video/ITEMID/FILENAME` (e.g., `filedir/59/e9/59e9c9a.../local_videoupload/video/1678888888/myvideo.mp4`). Where `USERID` is derived from the user's ID, and `ITEMID` is the timestamp used in `classes/external.php`. * Alternatively, you can query the `mdl_files` table in the Moodle database: ```sql SELECT * FROM mdl_files WHERE component = 'local_videoupload' AND filearea = 'video' ORDER BY timecreated DESC LIMIT 10; ``` This will show the details of the uploaded files. ### 8. Troubleshooting * **Plugin Not Appearing in Moodle App Menu:** * **Check Installation:** Ensure `local/videoupload` directory and all files are correctly placed. * **Moodle Upgrade:** Verify that you ran the Moodle upgrade process successfully and the plugin is listed in `Site administration > Plugins > Plugins overview`. * **Moodle App Cache:** Sometimes the Moodle App caches old site data. Try logging out and logging back into the app, or clearing the app's cache. * **`db/mobile.php` Syntax:** Double-check `local/videoupload/db/mobile.php` for any syntax errors. * **Video Upload Fails in App:** * **Server Logs:** Check your Moodle server's web server error logs (e.g., Apache `error_log`, Nginx `error.log`) and Moodle's `admin/tool/log/index.php` for any PHP errors or Moodle exceptions during the upload. * **Moodle `maxbytes`:** Ensure your Moodle site's `maxbytes` setting (Site administration > Server > System paths > Maximum upload file size) is large enough for the video files. * **PHP Configuration:** Verify your PHP `upload_max_filesize` and `post_max_size` settings in `php.ini` are sufficient. Restart your web server after changing `php.ini`. * **Disk Space:** Confirm your Moodle server has enough free disk space. * **`services.php` and `external.php`:** Ensure `local/videoupload/db/services.php` correctly defines `local_videoupload_save` and `local/videoupload/classes/external.php` implements the `save` method without errors. Check for typos in function names or class names. * **Network Issues:** Large video uploads can fail due to unstable network connections. * **Incorrect File Types Accepted/Rejected:** * Review the `filetypes` string in `local/videoupload/classes/output/mobile.php`. Ensure it lists the desired extensions correctly, separated by commas. ---"

The Solution

<r><p>Hi all,I just want to share this since I used the form knowledge to achieve it.Apparently it's a real struggle to use the build this in a custom plugin.So here it is FULLY working and tested.local/videoupload/templates/mobile_main.mustache&lt;ion-list&gt;&lt;ion-item lines="none"&gt;&lt;ion-label&gt;&lt;h2&gt;Videoupload&lt;/h2&gt;&lt;p&gt;Select 1 video and send it.&lt;/p&gt;&lt;/ion-label&gt;&lt;/ion-item&gt;&lt;core-attachments[files]="attachments"[maxSize]="CONTENT_OTHERDATA.maxsize"[maxSubmissions]="1"[component]="'local_videoupload'"[acceptedTypes]="CONTENT_OTHERDATA.filetypes"[allowOffline]="false"&gt;&lt;/core-attachments&gt;&lt;ion-buttonexpand="block"color="primary"(click)="sendVideo()"name="local_videoupload_send"&gt;Send&lt;/ion-button&gt;&lt;/ion-list&gt;local/videoupload/db/mobile.php&lt;?phpdefined('MOODLE_INTERNAL') <ISPOILER><s>||</s> die();$addons = ['local_videoupload' =&gt; ['handlers' =&gt; ['videoupload_menu' =&gt; ['delegate'    =&gt; 'CoreMainMenuDelegate','method'      =&gt; 'mobile_view','displaydata' =&gt; ['title' =&gt; 'pluginname','icon'  =&gt; 'fa-video',],'priority'    =&gt; 800,],],'lang' =&gt; [['pluginname', 'local_videoupload'],['send', 'local_videoupload'],['novideo', 'local_videoupload'],['uploadok', 'local_videoupload'],['uploadfail', 'local_videoupload'],],],];local/videoupload/classes/external.php&lt;?phpnamespace local_videoupload;use external_api;use external_function_parameters;use external_single_structure;use external_value;defined('MOODLE_INTERNAL') <e>||</e></ISPOILER> die();require_once($CFG-&gt;libdir . '/externallib.php');class external extends external_api {public static function save_parameters() {return new external_function_parameters(['draftitemid' =&gt; new external_value(PARAM_INT, 'Draft itemid from mobile upload'),]);}public static function save($draftitemid) {global $USER, $CFG;$params = self::validate_parameters(self::save_parameters(),['draftitemid' =&gt; $draftitemid]);$usercontext = \context_user::instance($USER-&gt;id);self::validate_context($usercontext);// Ensure file API functions are available.require_once($CFG-&gt;libdir . '/filelib.php');$finalitemid = time();\file_save_draft_area_files($params['draftitemid'],$usercontext-&gt;id,'local_videoupload','video',$finalitemid,['subdirs' =&gt; 0, 'maxfiles' =&gt; 1]);return ['status' =&gt; true,'itemid' =&gt; $finalitemid,];}public static function save_returns() {return new external_single_structure(['status' =&gt; new external_value(PARAM_BOOL, 'Success'),'itemid'  =&gt; new external_value(PARAM_INT, 'Stored itemid in plugin filearea'),]);}}ocal/videoupload/mobile/js/template_javascript.js/*** Moodle App site plugin JS.* Important: core-attachments expects to mutate a top-level array bound via [files].* So we use this.attachments (NOT CONTENT_OTHERDATA.attachments).*/// The array that &lt;core-attachments&gt; will update.this.attachments = [];this.sendVideo = async function () {const attachments = this.attachments <ISPOILER><s>||</s> [];if (!attachments.length) {this.CoreDomUtilsProvider.showToast(this.TranslateService.instant('local_videoupload.novideo'),true,4000);return false;}const modal = await this.CoreDomUtilsProvider.showModalLoading('core.sending', true);try {const uploadResult = await this.CoreFileUploaderProvider.uploadOrReuploadFiles(attachments);// Handle versions that return itemid as a number/string directly.let draftItemId = null;if (typeof uploadResult === 'number') {draftItemId = uploadResult;} else if (typeof uploadResult === 'string' &amp;&amp; uploadResult.trim() !== '' &amp;&amp; !isNaN(parseInt(uploadResult, 10))) {draftItemId = parseInt(uploadResult, 10);} else {draftItemId =(uploadResult &amp;&amp; uploadResult.itemid) <e>||</e></ISPOILER>(uploadResult &amp;&amp; uploadResult.draftitemid) <ISPOILER><s>||</s>(uploadResult &amp;&amp; uploadResult.draftid) <e>||</e></ISPOILER>(uploadResult &amp;&amp; uploadResult.data &amp;&amp; uploadResult.data.itemid) <ISPOILER><s>||</s>(uploadResult &amp;&amp; uploadResult.result &amp;&amp; uploadResult.result.itemid) <e>||</e></ISPOILER>(uploadResult &amp;&amp; uploadResult.filesresult &amp;&amp; uploadResult.filesresult.itemid) <ISPOILER><s>||</s>(uploadResult &amp;&amp; uploadResult[0] &amp;&amp; (uploadResult[0].itemid <e>||</e></ISPOILER> uploadResult[0].draftitemid));if (!draftItemId &amp;&amp; attachments[0]) {draftItemId =attachments[0].itemid <ISPOILER><s>||</s>attachments[0].draftitemid <e>||</e></ISPOILER>attachments[0].draftid;}}if (!draftItemId) {throw new Error('Upload finished but draft itemid was not returned.');}const site = await this.CoreSitesProvider.getSite();const result = await site.write('local_videoupload_save', { draftitemid: draftItemId });if (result &amp;&amp; result.status) {this.CoreDomUtilsProvider.showToast(this.TranslateService.instant('local_videoupload.uploadok'),true,6000);} else {this.CoreDomUtilsProvider.showToast(this.TranslateService.instant('local_videoupload.uploadfail'),true,6000);}return result;} catch (error) {this.CoreDomUtilsProvider.showErrorModalDefault(error, 'local_videoupload.uploadfail');return false;} finally {modal.dismiss();try { this.CoreFileUploaderProvider.clearTmpFiles(attachments); } catch (e) {}this.attachments = [];}};local/videoupload/classes/output/mobile.php&lt;?phpnamespace local_videoupload\output;defined('MOODLE_INTERNAL') <ISPOILER><s>||</s> die();class mobile {public static function mobile_view($args) {global $CFG, $OUTPUT;$maxbytes = isset($CFG-&gt;maxbytes) ? (int) $CFG-&gt;maxbytes : 0;// IMPORTANT: Keep otherdata flat + scalar strings (most compatible).$otherdata = ['maxsize'     =&gt; (string) $maxbytes,'filetypes'   =&gt; '.mp4,.mov,.m4v,.webm','attachments' =&gt; '', // empty string, JS will turn it into []];return ['templates' =&gt; [['id' =&gt; 'main','html' =&gt; $OUTPUT-&gt;render_from_template('local_videoupload/mobile_main', []),],],'javascript' =&gt; file_get_contents($CFG-&gt;dirroot. '/local/videoupload/mobile/js/template_javascript.js'),'otherdata' =&gt; $otherdata,];}}local/videoupload/db/services.php&lt;?phpdefined('MOODLE_INTERNAL') <e>||</e></ISPOILER> die();$functions = ['local_videoupload_save' =&gt; ['classname'   =&gt; 'local_videoupload\external','methodname'  =&gt; 'save','description' =&gt; 'Finalize a draft video upload.','type'        =&gt; 'write','services'    =&gt; [MOODLE_OFFICIAL_MOBILE_SERVICE],],];local/videoupload/version.php&lt;?phpdefined('MOODLE_INTERNAL') || die();$plugin-&gt;component = 'local_videoupload';$plugin-&gt;version   = 2025122906;$plugin-&gt;requires  = 2022041900; // Moodle 4.0+ (adjust if needed).</p> <HR>---</HR> <H3><s>### </s>&#129302; الحل الهندسي المقترح:</H3> <p>This Standard Operating Procedure (SOP) details how to implement and use a custom Moodle local plugin (<C><s>`</s>local_videoupload<e>`</e></C>) to enable video uploads directly from the Moodle official mobile app. The solution leverages Moodle's mobile API, file picker component (<C><s>`</s>core-attachments<e>`</e></C>), and external services to handle the upload process efficiently.</p> <HR>---</HR> <H2><s>## </s>Standard Operating Procedure: How to Upload a Video from Inside Moodle Official App Using a Custom Plugin and File Picker</H2> <H3><s>### </s>1. Introduction and Overview</H3> <p>This document provides a step-by-step guide for deploying and utilizing a custom Moodle local plugin, <C><s>`</s>local_videoupload<e>`</e></C>, designed to facilitate video uploads from the Moodle official mobile application. The plugin integrates with the mobile app's native file picker via the <C><s>`</s>core-attachments<e>`</e></C> component, uploads the selected video to a temporary (draft) area, and then moves it to a permanent file area associated with the plugin. This approach is robust and leverages Moodle's core file handling capabilities.</p> <H3><s>### </s>2. Prerequisites</H3> <LIST><LI><s>* </s>A running Moodle instance (Moodle 4.0 or newer is recommended based on the plugin's <C><s>`</s>requires<e>`</e></C> version <C><s>`</s>2022041900<e>`</e></C>).</LI> <LI><s>* </s>Administrator access to the Moodle site.</LI> <LI><s>* </s>Command-line (SSH) access to the Moodle server for plugin installation.</LI> <LI><s>* </s>The Moodle Official App installed on a mobile device (Android/iOS).</LI> <LI><s>* </s>Basic understanding of Moodle plugin structure and Linux filesystem navigation.</LI></LIST> <H3><s>### </s>3. Plugin Files and Structure</H3> <p>The custom plugin <C><s>`</s>local_videoupload<e>`</e></C> consists of the following files and directories. You will need to create this exact structure within your Moodle installation's <C><s>`</s>local/<e>`</e></C> directory.</p> <CODE><s>```</s><i> </i>moodle_root/ ├── local/ │ └── videoupload/ │ ├── classes/ │ │ ├── external.php │ │ └── output/ │ │ └── mobile.php │ ├── db/ │ │ ├── mobile.php │ │ └── services.php │ ├── lang/ │ │ └── en/ │ │ └── local_videoupload.php (This file is required, even if empty initially, or populated with strings) │ ├── mobile/ │ │ └── js/ │ │ └── template_javascript.js │ ├── templates/ │ │ └── mobile_main.mustache │ └── version.php<i> </i><e>```</e></CODE> <H3><s>### </s>4. Installation Steps</H3> <H4><s>#### </s>4.1. Create Plugin Directory and Files</H4> <LIST type="decimal"><LI><s>1. </s><p><STRONG><s>**</s>Connect to your Moodle server:<e>**</e></STRONG> Use SSH to connect to your Moodle server.</p> <CODE lang="bash"><s> ```bash</s><i> </i> ssh your_user@your_moodle_server_ip<i> </i><e> ```</e></CODE></LI> <LI><s>2. </s><p><STRONG><s>**</s>Navigate to Moodle's <C><s>`</s>local<e>`</e></C> directory:<e>**</e></STRONG></p> <CODE lang="bash"><s> ```bash</s><i> </i> cd /path/to/your/moodle/local<i> </i><e> ```</e></CODE> <p><EM><s>*</s>(Replace <C><s>`</s>/path/to/your/moodle<e>`</e></C> with the actual path to your Moodle installation directory, e.g., <C><s>`</s>/var/www/html/moodle<e>`</e></C>)<e>*</e></EM>.</p></LI> <LI><s>3. </s><p><STRONG><s>**</s>Create the plugin directory:<e>**</e></STRONG></p> <CODE lang="bash"><s> ```bash</s><i> </i> mkdir videoupload cd videoupload<i> </i><e> ```</e></CODE></LI> <LI><s>4. </s><p><STRONG><s>**</s>Create subdirectories:<e>**</e></STRONG></p> <CODE lang="bash"><s> ```bash</s><i> </i> mkdir -p classes/output db lang/en mobile/js templates<i> </i><e> ```</e></CODE></LI> <LI><s>5. </s><p><STRONG><s>**</s>Create the required language file:<e>**</e></STRONG> Even if empty, this file must exist for the <C><s>`</s>lang<e>`</e></C> entries in <C><s>`</s>db/mobile.php<e>`</e></C> to resolve correctly without warnings.</p> <CODE lang="bash"><s> ```bash</s><i> </i> touch lang/en/local_videoupload.php<i> </i><e> ```</e></CODE> <p><EM><s>*</s>(You can add the language strings later, e.g., <C><s>`</s>$string['pluginname'] = 'Video Upload';<e>`</e></C> etc.)<e>*</e></EM></p></LI> <LI><s>6. </s><p><STRONG><s>**</s>Populate files with provided code:<e>**</e></STRONG> For each file listed below, create it in the specified path and paste the corresponding code from the problem description.</p> <LIST><LI><s>* </s><p><C><s>`</s>local/videoupload/templates/mobile_main.mustache<e>`</e></C></p> <CODE lang="html"><s> ```html</s><i> </i> &lt;ion-list&gt; &lt;ion-item lines="none"&gt; &lt;ion-label&gt; &lt;h2&gt;Videoupload&lt;/h2&gt; &lt;p&gt;Select 1 video and send it.&lt;/p&gt; &lt;/ion-label&gt; &lt;/ion-item&gt; &lt;core-attachments [files]="attachments" [maxSize]="CONTENT_OTHERDATA.maxsize" [maxSubmissions]="1" [component]="'local_videoupload'" [acceptedTypes]="CONTENT_OTHERDATA.filetypes" [allowOffline]="false" &gt;&lt;/core-attachments&gt; &lt;ion-button expand="block" color="primary" (click)="sendVideo()" name="local_videoupload_send"&gt;Send&lt;/ion-button&gt; &lt;/ion-list&gt;<i> </i><e> ```</e></CODE></LI> <LI><s>* </s><p><C><s>`</s>local/videoupload/db/mobile.php<e>`</e></C></p> <CODE lang="php"><s> ```php</s><i> </i> &lt;?php defined('MOODLE_INTERNAL') || die(); $addons = [ 'local_videoupload' =&gt; [ 'handlers' =&gt; [ 'videoupload_menu' =&gt; [ 'delegate' =&gt; 'CoreMainMenuDelegate', 'method' =&gt; 'mobile_view', 'displaydata' =&gt; [ 'title' =&gt; 'pluginname', 'icon' =&gt; 'fa-video', ], 'priority' =&gt; 800, ], ], 'lang' =&gt; [ ['pluginname', 'local_videoupload'], ['send', 'local_videoupload'], ['novideo', 'local_videoupload'], ['uploadok', 'local_videoupload'], ['uploadfail', 'local_videoupload'], ], ], ];<i> </i><e> ```</e></CODE></LI> <LI><s>* </s><p><C><s>`</s>local/videoupload/classes/external.php<e>`</e></C></p> <CODE lang="php"><s> ```php</s><i> </i> &lt;?php namespace local_videoupload; use external_api; use external_function_parameters; use external_single_structure; use external_value; defined('MOODLE_INTERNAL') || die(); require_once($CFG-&gt;libdir . '/externallib.php'); class external extends external_api { public static function save_parameters() { return new external_function_parameters([ 'draftitemid' =&gt; new external_value(PARAM_INT, 'Draft itemid from mobile upload'), ]); } public static function save($draftitemid) { global $USER, $CFG; $params = self::validate_parameters(self::save_parameters(), [ 'draftitemid' =&gt; $draftitemid ]); $usercontext = \context_user::instance($USER-&gt;id); self::validate_context($usercontext); // Ensure file API functions are available. require_once($CFG-&gt;libdir . '/filelib.php'); $finalitemid = time(); // Use current timestamp as a unique itemid for this upload. \file_save_draft_area_files( $params['draftitemid'], $usercontext-&gt;id, 'local_videoupload', 'video', $finalitemid, ['subdirs' =&gt; 0, 'maxfiles' =&gt; 1] ); return [ 'status' =&gt; true, 'itemid' =&gt; $finalitemid, ]; } public static function save_returns() { return new external_single_structure([ 'status' =&gt; new external_value(PARAM_BOOL, 'Success'), 'itemid' =&gt; new external_value(PARAM_INT, 'Stored itemid in plugin filearea'), ]); } }<i> </i><e> ```</e></CODE></LI> <LI><s>* </s><p><C><s>`</s>local/videoupload/mobile/js/template_javascript.js<e>`</e></C></p> <CODE lang="javascript"><s> ```javascript</s><i> </i> /*** Moodle App site plugin JS. * Important: core-attachments expects to mutate a top-level array bound via [files]. * So we use this.attachments (NOT CONTENT_OTHERDATA.attachments). */ // The array that &lt;core-attachments&gt; will update. this.attachments = []; this.sendVideo = async function () { const attachments = this.attachments || []; if (!attachments.length) { this.CoreDomUtilsProvider.showToast(this.TranslateService.instant('local_videoupload.novideo'), true, 4000); return false; } const modal = await this.CoreDomUtilsProvider.showModalLoading('core.sending', true); try { const uploadResult = await this.CoreFileUploaderProvider.uploadOrReuploadFiles(attachments); // Handle versions that return itemid as a number/string directly. let draftItemId = null; if (typeof uploadResult === 'number') { draftItemId = uploadResult; } else if (typeof uploadResult === 'string' &amp;&amp; uploadResult.trim() !== '' &amp;&amp; !isNaN(parseInt(uploadResult, 10))) { draftItemId = parseInt(uploadResult, 10); } else { draftItemId = (uploadResult &amp;&amp; uploadResult.itemid) || (uploadResult &amp;&amp; uploadResult.draftitemid) || (uploadResult &amp;&amp; uploadResult.draftid) || (uploadResult &amp;&amp; uploadResult.data &amp;&amp; uploadResult.data.itemid) || (uploadResult &amp;&amp; uploadResult.result &amp;&amp; uploadResult.result.itemid) || (uploadResult &amp;&amp; uploadResult.filesresult &amp;&amp; uploadResult.filesresult.itemid) || (uploadResult &amp;&amp; uploadResult[0] &amp;&amp; (uploadResult[0].itemid || uploadResult[0].draftitemid)); if (!draftItemId &amp;&amp; attachments[0]) { draftItemId = attachments[0].itemid || attachments[0].draftitemid || attachments[0].draftid; } } if (!draftItemId) { throw new Error('Upload finished but draft itemid was not returned.'); } const site = await this.CoreSitesProvider.getSite(); const result = await site.write('local_videoupload_save', { draftitemid: draftItemId }); if (result &amp;&amp; result.status) { this.CoreDomUtilsProvider.showToast(this.TranslateService.instant('local_videoupload.uploadok'), true, 6000); } else { this.CoreDomUtilsProvider.showToast(this.TranslateService.instant('local_videoupload.uploadfail'), true, 6000); } return result; } catch (error) { this.CoreDomUtilsProvider.showErrorModalDefault(error, 'local_videoupload.uploadfail'); return false; } finally { modal.dismiss(); try { this.CoreFileUploaderProvider.clearTmpFiles(attachments); } catch (e) {} this.attachments = []; } };<i> </i><e> ```</e></CODE></LI> <LI><s>* </s><p><C><s>`</s>local/videoupload/classes/output/mobile.php<e>`</e></C></p> <CODE lang="php"><s> ```php</s><i> </i> &lt;?php namespace local_videoupload\output; defined('MOODLE_INTERNAL') || die(); class mobile { public static function mobile_view($args) { global $CFG, $OUTPUT; $maxbytes = isset($CFG-&gt;maxbytes) ? (int) $CFG-&gt;maxbytes : 0; // IMPORTANT: Keep otherdata flat + scalar strings (most compatible). $otherdata = [ 'maxsize' =&gt; (string) $maxbytes, 'filetypes' =&gt; '.mp4,.mov,.m4v,.webm', 'attachments' =&gt; '', // empty string, JS will turn it into [] ]; return [ 'templates' =&gt; [ [ 'id' =&gt; 'main', 'html' =&gt; $OUTPUT-&gt;render_from_template('local_videoupload/mobile_main', []), ], ], 'javascript' =&gt; file_get_contents($CFG-&gt;dirroot . '/local/videoupload/mobile/js/template_javascript.js'), 'otherdata' =&gt; $otherdata, ]; } }<i> </i><e> ```</e></CODE></LI> <LI><s>* </s><p><C><s>`</s>local/videoupload/db/services.php<e>`</e></C></p> <CODE lang="php"><s> ```php</s><i> </i> &lt;?php defined('MOODLE_INTERNAL') || die(); $functions = [ 'local_videoupload_save' =&gt; [ 'classname' =&gt; 'local_videoupload\external', 'methodname' =&gt; 'save', 'description' =&gt; 'Finalize a draft video upload.', 'type' =&gt; 'write', 'services' =&gt; [MOODLE_OFFICIAL_MOBILE_SERVICE], ], ];<i> </i><e> ```</e></CODE></LI> <LI><s>* </s><p><C><s>`</s>local/videoupload/version.php<e>`</e></C></p> <CODE lang="php"><s> ```php</s><i> </i> &lt;?php defined('MOODLE_INTERNAL') || die(); $plugin-&gt;component = 'local_videoupload'; $plugin-&gt;version = 2025122906; $plugin-&gt;requires = 2022041900; // Moodle 4.0+ (adjust if needed).<i> </i><e> ```</e></CODE></LI></LIST></LI></LIST> <H4><s>#### </s>4.2. Run Moodle Upgrade</H4> <LIST type="decimal"><LI><s>1. </s><STRONG><s>**</s>Access Moodle Administration:<e>**</e></STRONG> Open your web browser and navigate to your Moodle site's administration page (e.g., <C><s>`</s>http://your_moodle_site/admin<e>`</e></C>).</LI> <LI><s>2. </s><STRONG><s>**</s>Complete Installation:<e>**</e></STRONG> Moodle will detect the new plugin and prompt you to upgrade. Follow the on-screen instructions to complete the installation.</LI> <LI><s>3. </s><STRONG><s>**</s>Verify Plugin:<e>**</e></STRONG> Once the upgrade is complete, you can navigate to <C><s>`</s>Site administration &gt; Plugins &gt; Plugins overview<e>`</e></C> and search for "Video Upload" (or <C><s>`</s>local_videoupload<e>`</e></C>). Ensure it is listed and enabled.</LI></LIST> <H3><s>### </s>5. Code Explanation and Functionality Breakdown</H3> <p>This section explains how each part of the plugin contributes to the video upload functionality.</p> <LIST><LI><s>* </s><p><STRONG><s>**</s><C><s>`</s>local/videoupload/version.php<e>`</e></C><e>**</e></STRONG>:</p> <LIST><LI><s>* </s>Standard Moodle plugin metadata. It declares the plugin's component name (<C><s>`</s>local_videoupload<e>`</e></C>), its version (<C><s>`</s>2025122906<e>`</e></C>), and the minimum Moodle version it requires (<C><s>`</s>2022041900<e>`</e></C> for Moodle 4.0).</LI></LIST></LI> <LI><s>* </s><p><STRONG><s>**</s><C><s>`</s>local/videoupload/db/mobile.php<e>`</e></C><e>**</e></STRONG>:</p> <LIST><LI><s>* </s>This file registers the plugin's mobile capabilities.</LI> <LI><s>* </s>It defines a <C><s>`</s>videoupload_menu<e>`</e></C> handler that integrates the plugin into the Moodle App's main menu.</LI> <LI><s>* </s><C><s>`</s>delegate =&gt; 'CoreMainMenuDelegate'<e>`</e></C> ensures it appears in the main menu.</LI> <LI><s>* </s><C><s>`</s>method =&gt; 'mobile_view'<e>`</e></C> tells the Moodle App to call the <C><s>`</s>mobile_view<e>`</e></C> function in the <C><s>`</s>local_videoupload\output\mobile<e>`</e></C> class to render the page.</LI> <LI><s>* </s><C><s>`</s>displaydata<e>`</e></C> sets the title (using <C><s>`</s>pluginname<e>`</e></C> language string) and an icon (<C><s>`</s>fa-video<e>`</e></C>).</LI> <LI><s>* </s>The <C><s>`</s>lang<e>`</e></C> array lists all language strings used by the mobile plugin, ensuring they are downloaded to the app for offline use and translation.</LI></LIST></LI> <LI><s>* </s><p><STRONG><s>**</s><C><s>`</s>local/videoupload/db/services.php<e>`</e></C><e>**</e></STRONG>:</p> <LIST><LI><s>* </s>This file defines the Moodle Web Services function <C><s>`</s>local_videoupload_save<e>`</e></C>.</LI> <LI><s>* </s>It links this service name to the <C><s>`</s>save<e>`</e></C> method within the <C><s>`</s>local_videoupload\external<e>`</e></C> class.</LI> <LI><s>* </s><C><s>`</s>type =&gt; 'write'<e>`</e></C> indicates that this service modifies data.</LI> <LI><s>* </s><C><s>`</s>services =&gt; [MOODLE_OFFICIAL_MOBILE_SERVICE]<e>`</e></C> restricts this service to only be callable by the official Moodle Mobile App.</LI></LIST></LI> <LI><s>* </s><p><STRONG><s>**</s><C><s>`</s>local/videoupload/classes/external.php<e>`</e></C><e>**</e></STRONG>:</p> <LIST><LI><s>* </s>This class defines the backend logic for finalizing the video upload.</LI> <LI><s>* </s><C><s>`</s>save_parameters()<e>`</e></C>: Declares that the <C><s>`</s>save<e>`</e></C> function expects a single parameter, <C><s>`</s>draftitemid<e>`</e></C>, which is an integer representing the ID of the file(s) in Moodle's temporary file area.</LI> <LI><s>* </s><C><s>`</s>save($draftitemid)<e>`</e></C>: <LIST><LI><s>* </s>Validates the incoming <C><s>`</s>draftitemid<e>`</e></C>.</LI> <LI><s>* </s>Retrieves the current user's context.</LI> <LI><s>* </s>It uses <C><s>`</s>file_save_draft_area_files<e>`</e></C> to move the files from the temporary draft area to a permanent file area. <LIST><LI><s>* </s><C><s>`</s>$params['draftitemid']<e>`</e></C>: The ID of the files in the draft area.</LI> <LI><s>* </s><C><s>`</s>$usercontext-&gt;id<e>`</e></C>: The context ID where the files will be stored (here, the user context).</LI> <LI><s>* </s><C><s>`</s>'local_videoupload'<e>`</e></C>: The component name associated with these files.</LI> <LI><s>* </s><C><s>`</s>'video'<e>`</e></C>: The file area name within the component.</LI> <LI><s>* </s><C><s>`</s>$finalitemid<e>`</e></C>: A unique identifier (timestamp in this case) for this specific file group.</LI> <LI><s>* </s><C><s>`</s>['subdirs' =&gt; 0, 'maxfiles' =&gt; 1]<e>`</e></C>: Ensures no subdirectories are created and only one file is allowed.</LI></LIST></LI> <LI><s>* </s>Returns a status and the final <C><s>`</s>itemid<e>`</e></C> (which is the <C><s>`</s>$finalitemid<e>`</e></C> used above).</LI></LIST></LI></LIST></LI> <LI><s>* </s><p><STRONG><s>**</s><C><s>`</s>local/videoupload/classes/output/mobile.php<e>`</e></C><e>**</e></STRONG>:</p> <LIST><LI><s>* </s>This class is responsible for generating the content for the mobile view (<C><s>`</s>mobile_view<e>`</e></C> method, as defined in <C><s>`</s>db/mobile.php<e>`</e></C>).</LI> <LI><s>* </s>It retrieves Moodle's <C><s>`</s>maxbytes<e>`</e></C> setting to pass to the mobile app.</LI> <LI><s>* </s><C><s>`</s>$otherdata<e>`</e></C>: This array is crucial for passing configuration to the mobile app's JavaScript and template. <LIST><LI><s>* </s><C><s>`</s>maxsize<e>`</e></C>: The maximum file size allowed, derived from Moodle's <C><s>`</s>maxbytes<e>`</e></C>.</LI> <LI><s>* </s><C><s>`</s>filetypes<e>`</e></C>: Hardcoded list of accepted video file extensions.</LI> <LI><s>* </s><C><s>`</s>attachments<e>`</e></C>: An empty string, which the JavaScript will interpret as an empty array for <C><s>`</s>core-attachments<e>`</e></C>.</LI></LIST></LI> <LI><s>* </s>It renders the <C><s>`</s>mobile_main.mustache<e>`</e></C> template.</LI> <LI><s>* </s>It injects the JavaScript from <C><s>`</s>template_javascript.js<e>`</e></C> directly into the mobile page.</LI></LIST></LI> <LI><s>* </s><p><STRONG><s>**</s><C><s>`</s>local/videoupload/templates/mobile_main.mustache<e>`</e></C><e>**</e></STRONG>:</p> <LIST><LI><s>* </s>This is the Mustache template that defines the user interface within the Moodle mobile app.</LI> <LI><s>* </s><C><s>`</s>&lt;core-attachments&gt;<e>`</e></C>: This is a Moodle mobile app component that provides a standardized file picker interface. <LIST><LI><s>* </s><C><s>`</s>[files]="attachments"<e>`</e></C>: Binds the component to the <C><s>`</s>attachments<e>`</e></C> array in the JavaScript, which holds the selected files.</LI> <LI><s>* </s><C><s>`</s>[maxSize]="CONTENT_OTHERDATA.maxsize"<e>`</e></C>: Passes the maximum file size limit from the PHP output.</LI> <LI><s>* </s><C><s>`</s>[maxSubmissions]="1"<e>`</e></C>: Allows only one file to be selected.</LI> <LI><s>* </s><C><s>`</s>[component]="'local_videoupload'"<e>`</e></C>: Associates the upload with this plugin's component.</LI> <LI><s>* </s><C><s>`</s>[acceptedTypes]="CONTENT_OTHERDATA.filetypes"<e>`</e></C>: Specifies allowed file types.</LI></LIST></LI> <LI><s>* </s><C><s>`</s>&lt;ion-button (click)="sendVideo()"&gt;<e>`</e></C>: A button that, when clicked, calls the <C><s>`</s>sendVideo<e>`</e></C> function defined in the JavaScript.</LI></LIST></LI> <LI><s>* </s><p><STRONG><s>**</s><C><s>`</s>local/videoupload/mobile/js/template_javascript.js<e>`</e></C><e>**</e></STRONG>:</p> <LIST><LI><s>* </s>This is the client-side logic for the mobile app page.</LI> <LI><s>* </s><C><s>`</s>this.attachments = []<e>`</e></C>: Initializes the array that <C><s>`</s>core-attachments<e>`</e></C> will populate with selected files.</LI> <LI><s>* </s><C><s>`</s>this.sendVideo = async function ()<e>`</e></C>: The asynchronous function triggered by the "Send" button. <LIST><LI><s>* </s><STRONG><s>**</s>Validation<e>**</e></STRONG>: Checks if any files have been selected. If not, shows a toast message.</LI> <LI><s>* </s><STRONG><s>**</s>Loading Modal<e>**</e></STRONG>: Displays a "Sending..." modal during the upload process.</LI> <LI><s>* </s><STRONG><s>**</s><C><s>`</s>CoreFileUploaderProvider.uploadOrReuploadFiles(attachments)<e>`</e></C><e>**</e></STRONG>: This is the core mobile API call to upload the selected files to Moodle's temporary (draft) file area. It returns a <C><s>`</s>draftitemid<e>`</e></C>.</LI> <LI><s>* </s><STRONG><s>**</s><C><s>`</s>draftItemId<e>`</e></C> extraction<e>**</e></STRONG>: The code includes robust logic to extract the <C><s>`</s>draftitemid<e>`</e></C> from various possible return formats of <C><s>`</s>uploadOrReuploadFiles<e>`</e></C>, making it compatible with different Moodle App versions or edge cases.</LI> <LI><s>* </s><STRONG><s>**</s><C><s>`</s>site.write('local_videoupload_save', { draftitemid: draftItemId })<e>`</e></C><e>**</e></STRONG>: This is where the Moodle App calls the custom external function <C><s>`</s>local_videoupload_save<e>`</e></C> (defined in <C><s>`</s>db/services.php<e>`</e></C> and implemented in <C><s>`</s>classes/external.php<e>`</e></C>) to finalize the upload by moving the files from the draft area to their permanent location.</LI> <LI><s>* </s><STRONG><s>**</s>Feedback<e>**</e></STRONG>: Shows success or failure toast messages based on the result of the <C><s>`</s>site.write<e>`</e></C> call.</LI> <LI><s>* </s><STRONG><s>**</s>Cleanup<e>**</e></STRONG>: <LIST><LI><s>* </s><C><s>`</s>modal.dismiss()<e>`</e></C>: Hides the loading modal.</LI> <LI><s>* </s><C><s>`</s>CoreFileUploaderProvider.clearTmpFiles(attachments)<e>`</e></C>: Clears temporary files from the mobile app's cache.</LI> <LI><s>* </s><C><s>`</s>this.attachments = []<e>`</e></C>: Resets the attachments array to clear the file picker.</LI></LIST></LI></LIST></LI></LIST></LI></LIST> <H3><s>### </s>6. Usage from Moodle Mobile App</H3> <LIST type="decimal"><LI><s>1. </s><STRONG><s>**</s>Open Moodle Official App:<e>**</e></STRONG> Launch the Moodle App on your mobile device.</LI> <LI><s>2. </s><STRONG><s>**</s>Connect to your Moodle site:<e>**</e></STRONG> Ensure you are logged into your Moodle site where the plugin was installed.</LI> <LI><s>3. </s><STRONG><s>**</s>Navigate to the Main Menu:<e>**</e></STRONG> Access the main menu (usually a hamburger icon <C><s>`</s>☰<e>`</e></C>).</LI> <LI><s>4. </s><STRONG><s>**</s>Find "Videoupload":<e>**</e></STRONG> Look for an entry titled "Videoupload" (or the translated version if you added language strings) with a video camera icon (&#127916; or similar). Tap on it.</LI> <LI><s>5. </s><STRONG><s>**</s>Select a Video:<e>**</e></STRONG> <LIST><LI><s>* </s>The "Videoupload" page will open, displaying "Select 1 video and send it."</LI> <LI><s>* </s>Tap on the file picker area (it usually shows "Add file..." or a plus icon).</LI> <LI><s>* </s>Your device's file picker or gallery will open. Select a single video file (e.g., MP4, MOV).</LI></LIST></LI> <LI><s>6. </s><STRONG><s>**</s>Send the Video:<e>**</e></STRONG> <LIST><LI><s>* </s>Once the video is selected, its name will appear in the attachments area.</LI> <LI><s>* </s>Tap the "Send" button.</LI></LIST></LI> <LI><s>7. </s><STRONG><s>**</s>Monitor Upload Progress:<e>**</e></STRONG> <LIST><LI><s>* </s>A "Sending..." loading modal will appear.</LI> <LI><s>* </s>Upon completion, a toast message will confirm "Upload OK" or "Upload Failed."</LI> <LI><s>* </s>The file picker will clear, indicating the process is complete.</LI></LIST></LI></LIST> <H3><s>### </s>7. Verification</H3> <p>After a successful upload:</p> <LIST type="decimal"><LI><s>1. </s><STRONG><s>**</s>Check Moodle Server Files:<e>**</e></STRONG> <LIST><LI><s>* </s>Connect to your Moodle server via SSH.</LI> <LI><s>* </s>Navigate to your Moodle data directory (e.g., <C><s>`</s>/path/to/your/moodledata<e>`</e></C>).</LI> <LI><s>* </s>Look for files stored by the plugin. They should be in a path similar to:<br/> <C><s>`</s>filedir/U/S/USERID/local_videoupload/video/ITEMID/FILENAME<e>`</e></C><br/> (e.g., <C><s>`</s>filedir/59/e9/59e9c9a.../local_videoupload/video/1678888888/myvideo.mp4<e>`</e></C>).<br/> Where <C><s>`</s>USERID<e>`</e></C> is derived from the user's ID, and <C><s>`</s>ITEMID<e>`</e></C> is the timestamp used in <C><s>`</s>classes/external.php<e>`</e></C>.</LI> <LI><s>* </s>Alternatively, you can query the <C><s>`</s>mdl_files<e>`</e></C> table in the Moodle database: <CODE lang="sql"><s> ```sql</s><i> </i> SELECT * FROM mdl_files WHERE component = 'local_videoupload' AND filearea = 'video' ORDER BY timecreated DESC LIMIT 10;<i> </i><e> ```</e></CODE> This will show the details of the uploaded files.</LI></LIST></LI></LIST> <H3><s>### </s>8. Troubleshooting</H3> <LIST><LI><s>* </s><p><STRONG><s>**</s>Plugin Not Appearing in Moodle App Menu:<e>**</e></STRONG></p> <LIST><LI><s>* </s><STRONG><s>**</s>Check Installation:<e>**</e></STRONG> Ensure <C><s>`</s>local/videoupload<e>`</e></C> directory and all files are correctly placed.</LI> <LI><s>* </s><STRONG><s>**</s>Moodle Upgrade:<e>**</e></STRONG> Verify that you ran the Moodle upgrade process successfully and the plugin is listed in <C><s>`</s>Site administration &gt; Plugins &gt; Plugins overview<e>`</e></C>.</LI> <LI><s>* </s><STRONG><s>**</s>Moodle App Cache:<e>**</e></STRONG> Sometimes the Moodle App caches old site data. Try logging out and logging back into the app, or clearing the app's cache.</LI> <LI><s>* </s><STRONG><s>**</s><C><s>`</s>db/mobile.php<e>`</e></C> Syntax:<e>**</e></STRONG> Double-check <C><s>`</s>local/videoupload/db/mobile.php<e>`</e></C> for any syntax errors.</LI></LIST></LI> <LI><s>* </s><p><STRONG><s>**</s>Video Upload Fails in App:<e>**</e></STRONG></p> <LIST><LI><s>* </s><STRONG><s>**</s>Server Logs:<e>**</e></STRONG> Check your Moodle server's web server error logs (e.g., Apache <C><s>`</s>error_log<e>`</e></C>, Nginx <C><s>`</s>error.log<e>`</e></C>) and Moodle's <C><s>`</s>admin/tool/log/index.php<e>`</e></C> for any PHP errors or Moodle exceptions during the upload.</LI> <LI><s>* </s><STRONG><s>**</s>Moodle <C><s>`</s>maxbytes<e>`</e></C>:<e>**</e></STRONG> Ensure your Moodle site's <C><s>`</s>maxbytes<e>`</e></C> setting (Site administration &gt; Server &gt; System paths &gt; Maximum upload file size) is large enough for the video files.</LI> <LI><s>* </s><STRONG><s>**</s>PHP Configuration:<e>**</e></STRONG> Verify your PHP <C><s>`</s>upload_max_filesize<e>`</e></C> and <C><s>`</s>post_max_size<e>`</e></C> settings in <C><s>`</s>php.ini<e>`</e></C> are sufficient. Restart your web server after changing <C><s>`</s>php.ini<e>`</e></C>.</LI> <LI><s>* </s><STRONG><s>**</s>Disk Space:<e>**</e></STRONG> Confirm your Moodle server has enough free disk space.</LI> <LI><s>* </s><STRONG><s>**</s><C><s>`</s>services.php<e>`</e></C> and <C><s>`</s>external.php<e>`</e></C>:<e>**</e></STRONG> Ensure <C><s>`</s>local/videoupload/db/services.php<e>`</e></C> correctly defines <C><s>`</s>local_videoupload_save<e>`</e></C> and <C><s>`</s>local/videoupload/classes/external.php<e>`</e></C> implements the <C><s>`</s>save<e>`</e></C> method without errors. Check for typos in function names or class names.</LI> <LI><s>* </s><STRONG><s>**</s>Network Issues:<e>**</e></STRONG> Large video uploads can fail due to unstable network connections.</LI></LIST></LI> <LI><s>* </s><p><STRONG><s>**</s>Incorrect File Types Accepted/Rejected:<e>**</e></STRONG></p> <LIST><LI><s>* </s>Review the <C><s>`</s>filetypes<e>`</e></C> string in <C><s>`</s>local/videoupload/classes/output/mobile.php<e>`</e></C>. Ensure it lists the desired extensions correctly, separated by commas.</LI></LIST></LI></LIST> <HR>---</HR></r>

Did this solution help you resolve the issue?