diff --git a/README.md b/README.md index a77de3e6..1636f8b6 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ A demo is available at https://demo.mediacms.io - **Configurable actions**: allow download, add comments, add likes, dislikes, report media - **Configuration options**: change logos, fonts, styling, add more pages - **Enhanced video player**: customized video.js player with multiple resolution and playback speed options -- **Multiple transcoding profiles**: sane defaults for multiple dimensions (240p, 360p, 480p, 720p, 1080p) and multiple profiles (h264, h265, vp9) +- **Multiple transcoding profiles**: sane defaults for multiple dimensions (144p, 240p, 360p, 480p, 720p, 1080p) and multiple profiles (h264, h265, vp9) - **Adaptive video streaming**: possible through HLS protocol - **Subtitles/CC**: support for multilingual subtitle files - **Scalable transcoding**: transcoding through priorities. Experimental support for remote workers @@ -93,20 +93,14 @@ There are two ways to run MediaCMS, through Docker Compose and through installin A complete guide can be found on the blog post [How to self-host and share your videos in 2021](https://medium.com/@MediaCMS.io/how-to-self-host-and-share-your-videos-in-2021-14067e3b291b). -## Configuration - -Visit [Configuration](docs/admins_docs.md#5-configuration) page. - - -## Information for developers -Check out the new section on the [Developer Experience](docs/dev_exp.md) page - - ## Documentation * [Users documentation](docs/user_docs.md) page * [Administrators documentation](docs/admins_docs.md) page * [Developers documentation](docs/developers_docs.md) page +* [Configuration](docs/admins_docs.md#5-configuration) page +* [Transcoding](docs/transcoding.md) page +* [Developer Experience](docs/dev_exp.md) page ## Technology diff --git a/cms/settings.py b/cms/settings.py index 21da2fa5..075c2518 100644 --- a/cms/settings.py +++ b/cms/settings.py @@ -186,7 +186,7 @@ CHUNKIZE_VIDEO_DURATION = 60 * 5 VIDEO_CHUNKS_DURATION = 60 * 4 # always get these two, even if upscaling -MINIMUM_RESOLUTIONS_TO_ENCODE = [240, 360] +MINIMUM_RESOLUTIONS_TO_ENCODE = [144, 240] # default settings for notifications # not all of them are implemented @@ -497,6 +497,10 @@ USE_ROUNDED_CORNERS = True ALLOW_VIDEO_TRIMMER = True ALLOW_CUSTOM_MEDIA_URLS = False + +# ffmpeg options +FFMPEG_DEFAULT_PRESET = "medium" # see https://trac.ffmpeg.org/wiki/Encode/H.264 + try: # keep a local_settings.py file for local overrides from .local_settings import * # noqa diff --git a/docker-compose.yaml b/docker-compose.yaml index 4a42b5ea..888023cd 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -72,7 +72,7 @@ services: POSTGRES_DB: mediacms TZ: Europe/London healthcheck: - test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}", "--host=db", "--dbname=$POSTGRES_DB", "--username=$POSTGRES_USER"] + test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"] interval: 10s timeout: 5s retries: 5 @@ -81,6 +81,6 @@ services: restart: always healthcheck: test: ["CMD", "redis-cli","ping"] - interval: 30s - timeout: 10s + interval: 10s + timeout: 5s retries: 3 diff --git a/docs/transcoding.md b/docs/transcoding.md new file mode 100644 index 00000000..11b2359d --- /dev/null +++ b/docs/transcoding.md @@ -0,0 +1,50 @@ +# Transcoding in MediaCMS + +MediaCMS uses FFmpeg for transcoding media files. Most of the transcoding settings and configurations are defined in `files/helpers.py`. + +## Configuration Options + +Several transcoding parameters can be customized in `cms/settings.py`: + +### FFmpeg Preset + +The default FFmpeg preset is set to "medium". This setting controls the encoding speed and compression efficiency trade-off. + +```python +# ffmpeg options +FFMPEG_DEFAULT_PRESET = "medium" # see https://trac.ffmpeg.org/wiki/Encode/H.264 +``` + +Available presets include: +- ultrafast +- superfast +- veryfast +- faster +- fast +- medium (default) +- slow +- slower +- veryslow + +Faster presets result in larger file sizes for the same quality, while slower presets provide better compression but take longer to encode. + +### Other Transcoding Settings + +Additional transcoding settings in `settings.py` include: + +- `FFMPEG_COMMAND`: Path to the FFmpeg executable +- `FFPROBE_COMMAND`: Path to the FFprobe executable +- `DO_NOT_TRANSCODE_VIDEO`: If set to True, only the original video is shown without transcoding +- `CHUNKIZE_VIDEO_DURATION`: For videos longer than this duration (in seconds), they get split into chunks and encoded independently +- `VIDEO_CHUNKS_DURATION`: Duration of each chunk (must be smaller than CHUNKIZE_VIDEO_DURATION) +- `MINIMUM_RESOLUTIONS_TO_ENCODE`: Always encode these resolutions, even if upscaling is required + +## Advanced Configuration + +For more advanced transcoding settings, you may need to modify the following in `files/helpers.py`: + +- Video bitrates for different codecs and resolutions +- Audio encoders and bitrates +- CRF (Constant Rate Factor) values +- Keyframe settings +- Encoding parameters for different codecs (H.264, H.265, VP9) diff --git a/files/helpers.py b/files/helpers.py index a9eddd3f..1884c4ad 100644 --- a/files/helpers.py +++ b/files/helpers.py @@ -34,12 +34,6 @@ BUF_SIZE_MULTIPLIER = 1.5 KEYFRAME_DISTANCE = 4 KEYFRAME_DISTANCE_MIN = 2 -# speed presets -# see https://trac.ffmpeg.org/wiki/Encode/H.264 -X26x_PRESET = "medium" # "medium" -X265_PRESET = "medium" -X26x_PRESET_BIG_HEIGHT = "faster" - # VP9_SPEED = 1 # between 0 and 4, lower is slower VP9_SPEED = 2 @@ -55,6 +49,7 @@ VIDEO_CRFS = { VIDEO_BITRATES = { "h264": { 25: { + 144: 150, 240: 300, 360: 500, 480: 1000, @@ -67,6 +62,7 @@ VIDEO_BITRATES = { }, "h265": { 25: { + 144: 75, 240: 150, 360: 275, 480: 500, @@ -79,6 +75,7 @@ VIDEO_BITRATES = { }, "vp9": { 25: { + 144: 75, 240: 150, 360: 275, 480: 500, @@ -596,17 +593,13 @@ def get_base_ffmpeg_command( cmd = base_cmd[:] # preset settings + preset = getattr(settings, "FFMPEG_DEFAULT_PRESET", "medium") + if encoder == "libvpx-vp9": if pass_number == 1: speed = 4 else: speed = VP9_SPEED - elif encoder in ["libx264"]: - preset = X26x_PRESET - elif encoder in ["libx265"]: - preset = X265_PRESET - if target_height >= 720: - preset = X26x_PRESET_BIG_HEIGHT if encoder == "libx264": level = "4.2" if target_height <= 1080 else "5.2" @@ -730,7 +723,7 @@ def produce_ffmpeg_commands(media_file, media_info, resolution, codec, output_fi return False if media_info.get("video_height") < resolution: - if resolution not in [240, 360]: # always get these two + if resolution not in settings.MINIMUM_RESOLUTIONS_TO_ENCODE: return False # if codec == "h264_baseline": diff --git a/files/migrations/0010_alter_encodeprofile_resolution.py b/files/migrations/0010_alter_encodeprofile_resolution.py new file mode 100644 index 00000000..8a520ca8 --- /dev/null +++ b/files/migrations/0010_alter_encodeprofile_resolution.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.6 on 2025-07-05 11:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('files', '0009_alter_media_friendly_token'), + ] + + operations = [ + migrations.AlterField( + model_name='encodeprofile', + name='resolution', + field=models.IntegerField(blank=True, choices=[(2160, '2160'), (1440, '1440'), (1080, '1080'), (720, '720'), (480, '480'), (360, '360'), (240, '240'), (144, '144')], null=True), + ), + ] diff --git a/files/models.py b/files/models.py index 2ca69496..1729094b 100644 --- a/files/models.py +++ b/files/models.py @@ -73,6 +73,7 @@ ENCODE_RESOLUTIONS = ( (480, "480"), (360, "360"), (240, "240"), + (144, "144"), ) CODECS = ( @@ -896,7 +897,7 @@ class Media(models.Model): """ res = {} - valid_resolutions = [240, 360, 480, 720, 1080, 1440, 2160] + valid_resolutions = [144, 240, 360, 480, 720, 1080, 1440, 2160] if self.hls_file: if os.path.exists(self.hls_file): hls_file = self.hls_file diff --git a/fixtures/encoding_profiles.json b/fixtures/encoding_profiles.json index e7eccafa..7e68b37b 100644 --- a/fixtures/encoding_profiles.json +++ b/fixtures/encoding_profiles.json @@ -1 +1 @@ -[{"model": "files.encodeprofile", "pk": 19, "fields": {"name": "h264-2160", "extension": "mp4", "resolution": 2160, "codec": "h264", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 22, "fields": {"name": "vp9-2160", "extension": "webm", "resolution": 2160, "codec": "vp9", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 16, "fields": {"name": "h265-2160", "extension": "mp4", "resolution": 2160, "codec": "h265", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 4, "fields": {"name": "h264-1440", "extension": "mp4", "resolution": 1440, "codec": "h264", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 5, "fields": {"name": "vp9-1440", "extension": "webm", "resolution": 1440, "codec": "vp9", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 6, "fields": {"name": "h265-1440", "extension": "mp4", "resolution": 1440, "codec": "h265", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 7, "fields": {"name": "h264-1080", "extension": "mp4", "resolution": 1080, "codec": "h264", "description": "", "active": true}}, {"model": "files.encodeprofile", "pk": 8, "fields": {"name": "vp9-1080", "extension": "webm", "resolution": 1080, "codec": "vp9", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 9, "fields": {"name": "h265-1080", "extension": "mp4", "resolution": 1080, "codec": "h265", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 10, "fields": {"name": "h264-720", "extension": "mp4", "resolution": 720, "codec": "h264", "description": "", "active": true}}, {"model": "files.encodeprofile", "pk": 11, "fields": {"name": "vp9-720", "extension": "webm", "resolution": 720, "codec": "vp9", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 12, "fields": {"name": "h265-720", "extension": "mp4", "resolution": 720, "codec": "h265", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 13, "fields": {"name": "h264-480", "extension": "mp4", "resolution": 480, "codec": "h264", "description": "", "active": true}}, {"model": "files.encodeprofile", "pk": 14, "fields": {"name": "vp9-480", "extension": "webm", "resolution": 480, "codec": "vp9", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 15, "fields": {"name": "h265-480", "extension": "mp4", "resolution": 480, "codec": "h265", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 3, "fields": {"name": "h264-360", "extension": "mp4", "resolution": 360, "codec": "h264", "description": "", "active": true}}, {"model": "files.encodeprofile", "pk": 17, "fields": {"name": "vp9-360", "extension": "webm", "resolution": 360, "codec": "vp9", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 18, "fields": {"name": "h265-360", "extension": "mp4", "resolution": 360, "codec": "h265", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 2, "fields": {"name": "h264-240", "extension": "mp4", "resolution": 240, "codec": "h264", "description": "", "active": true}}, {"model": "files.encodeprofile", "pk": 20, "fields": {"name": "vp9-240", "extension": "webm", "resolution": 240, "codec": "vp9", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 21, "fields": {"name": "h265-240", "extension": "mp4", "resolution": 240, "codec": "h265", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 1, "fields": {"name": "preview", "extension": "gif", "resolution": null, "codec": null, "description": "", "active": true}}] +[{"model": "files.encodeprofile", "pk": 19, "fields": {"name": "h264-2160", "extension": "mp4", "resolution": 2160, "codec": "h264", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 22, "fields": {"name": "vp9-2160", "extension": "webm", "resolution": 2160, "codec": "vp9", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 23, "fields": {"name": "h264-144", "extension": "mp4", "resolution": 144, "codec": "h264", "description": "", "active": true}}, {"model": "files.encodeprofile", "pk": 16, "fields": {"name": "h265-2160", "extension": "mp4", "resolution": 2160, "codec": "h265", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 4, "fields": {"name": "h264-1440", "extension": "mp4", "resolution": 1440, "codec": "h264", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 5, "fields": {"name": "vp9-1440", "extension": "webm", "resolution": 1440, "codec": "vp9", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 6, "fields": {"name": "h265-1440", "extension": "mp4", "resolution": 1440, "codec": "h265", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 7, "fields": {"name": "h264-1080", "extension": "mp4", "resolution": 1080, "codec": "h264", "description": "", "active": true}}, {"model": "files.encodeprofile", "pk": 8, "fields": {"name": "vp9-1080", "extension": "webm", "resolution": 1080, "codec": "vp9", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 9, "fields": {"name": "h265-1080", "extension": "mp4", "resolution": 1080, "codec": "h265", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 10, "fields": {"name": "h264-720", "extension": "mp4", "resolution": 720, "codec": "h264", "description": "", "active": true}}, {"model": "files.encodeprofile", "pk": 11, "fields": {"name": "vp9-720", "extension": "webm", "resolution": 720, "codec": "vp9", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 12, "fields": {"name": "h265-720", "extension": "mp4", "resolution": 720, "codec": "h265", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 13, "fields": {"name": "h264-480", "extension": "mp4", "resolution": 480, "codec": "h264", "description": "", "active": true}}, {"model": "files.encodeprofile", "pk": 14, "fields": {"name": "vp9-480", "extension": "webm", "resolution": 480, "codec": "vp9", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 15, "fields": {"name": "h265-480", "extension": "mp4", "resolution": 480, "codec": "h265", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 3, "fields": {"name": "h264-360", "extension": "mp4", "resolution": 360, "codec": "h264", "description": "", "active": true}}, {"model": "files.encodeprofile", "pk": 17, "fields": {"name": "vp9-360", "extension": "webm", "resolution": 360, "codec": "vp9", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 18, "fields": {"name": "h265-360", "extension": "mp4", "resolution": 360, "codec": "h265", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 2, "fields": {"name": "h264-240", "extension": "mp4", "resolution": 240, "codec": "h264", "description": "", "active": true}}, {"model": "files.encodeprofile", "pk": 20, "fields": {"name": "vp9-240", "extension": "webm", "resolution": 240, "codec": "vp9", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 21, "fields": {"name": "h265-240", "extension": "mp4", "resolution": 240, "codec": "h265", "description": "", "active": false}}, {"model": "files.encodeprofile", "pk": 1, "fields": {"name": "preview", "extension": "gif", "resolution": null, "codec": null, "description": "", "active": true}}] diff --git a/tests/api/test_new_media.py b/tests/api/test_new_media.py index 57ea9967..77e38e55 100644 --- a/tests/api/test_new_media.py +++ b/tests/api/test_new_media.py @@ -43,8 +43,8 @@ class TestX(TestCase): self.assertEqual(Media.objects.filter(media_type='image').count(), 1, "Media identification failed") self.assertEqual(Media.objects.filter(user=self.user).count(), 3, "User assignment failed") medium_video = Media.objects.get(title="medium_video.mp4") - self.assertEqual(len(medium_video.hls_info), 11, "Problem with HLS info") + self.assertEqual(len(medium_video.hls_info), 13, "Problem with HLS info") # using the provided EncodeProfiles, these two files should produce 9 Encoding objects. # if new EncodeProfiles are added and enabled, this will break! - self.assertEqual(Encoding.objects.filter(status='success').count(), 9, "Not all video transcodings finished well") + self.assertEqual(Encoding.objects.filter(status='success').count(), 10, "Not all video transcodings finished well") diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index 145e8ed6..a7723376 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -24,12 +24,12 @@ class TestFixtures(TestCase): profiles = EncodeProfile.objects.all() self.assertEqual( profiles.count(), - 22, + 23, "Problem with Encode Profile fixtures", ) profiles = EncodeProfile.objects.filter(active=True) self.assertEqual( profiles.count(), - 6, + 7, "Problem with Encode Profile fixtures, not as active as expected", )