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<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') <ISPOILER><s>||</s> 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') <e>||</e></ISPOILER> 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 <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' && uploadResult.trim() !== '' && !isNaN(parseInt(uploadResult, 10))) {draftItemId = parseInt(uploadResult, 10);} else {draftItemId =(uploadResult && uploadResult.itemid) <e>||</e></ISPOILER>(uploadResult && uploadResult.draftitemid) <ISPOILER><s>||</s>(uploadResult && uploadResult.draftid) <e>||</e></ISPOILER>(uploadResult && uploadResult.data && uploadResult.data.itemid) <ISPOILER><s>||</s>(uploadResult && uploadResult.result && uploadResult.result.itemid) <e>||</e></ISPOILER>(uploadResult && uploadResult.filesresult && uploadResult.filesresult.itemid) <ISPOILER><s>||</s>(uploadResult && uploadResult[0] && (uploadResult[0].itemid <e>||</e></ISPOILER> uploadResult[0].draftitemid));if (!draftItemId && 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 && 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') <ISPOILER><s>||</s> 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') <e>||</e></ISPOILER> 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).</p>
<HR>---</HR>
<H3><s>### </s>🤖 الحل الهندسي المقترح:</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> <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><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> <?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'],
],
],
];<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> <?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'),
]);
}
}<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 <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 = [];
}
};<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> <?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,
];
}
}<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> <?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],
],
];<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> <?php
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'local_videoupload';
$plugin->version = 2025122906;
$plugin->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 > Plugins > 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 => 'CoreMainMenuDelegate'<e>`</e></C> ensures it appears in the main menu.</LI>
<LI><s>* </s><C><s>`</s>method => '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 => 'write'<e>`</e></C> indicates that this service modifies data.</LI>
<LI><s>* </s><C><s>`</s>services => [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->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' => 0, 'maxfiles' => 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><core-attachments><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><ion-button (click)="sendVideo()"><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 (🎬 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 > Plugins > 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 > Server > System paths > 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?