• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

EsupPortail / Esup-Pod / 21987813068

13 Feb 2026 01:02PM UTC coverage: 68.071%. First build
21987813068

Pull #1403

github

web-flow
Merge e69fc8299 into fe69b2ede
Pull Request #1403: Runner Manager support

614 of 1433 new or added lines in 24 files covered. (42.85%)

12794 of 18795 relevant lines covered (68.07%)

0.68 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

71.53
/pod/video_encode_transcript/encode.py
1
"""Esup-Pod module to handle video encoding with CPU."""
2

3
import logging
1✔
4
import threading
1✔
5
import time
1✔
6

7
from django.conf import settings
1✔
8
from webpush.models import PushInformation
1✔
9

10
from pod.cut.models import CutVideo
1✔
11
from pod.dressing.models import Dressing
1✔
12
from pod.dressing.utils import get_dressing_input
1✔
13
from pod.main.tasks import task_start_encode, task_start_encode_studio
1✔
14
from pod.recorder.models import Recording
1✔
15
from pod.video.models import Video
1✔
16

17
from .encoding_settings import FFMPEG_DRESSING_INPUT
1✔
18
from .encoding_studio import start_encode_video_studio
1✔
19
from .Encoding_video_model import Encoding_video_model
1✔
20
from .models import EncodingLog
1✔
21
from .utils import (
1✔
22
    add_encoding_log,
23
    change_encoding_step,
24
    check_file,
25
    send_email,
26
    send_email_encoding,
27
    send_email_recording,
28
    send_notification_encoding,
29
    time_to_seconds,
30
)
31

32
__license__ = "LGPL v3"
1✔
33
log = logging.getLogger(__name__)
1✔
34

35
USE_TRANSCRIPTION = getattr(settings, "USE_TRANSCRIPTION", False)
1✔
36
USE_NOTIFICATIONS = getattr(settings, "USE_NOTIFICATIONS", False)
1✔
37
if USE_TRANSCRIPTION:
1✔
38
    from . import transcript
1✔
39

40
    TRANSCRIPT_VIDEO = getattr(settings, "TRANSCRIPT_VIDEO", "start_transcript")
1✔
41

42
CELERY_TO_ENCODE = getattr(settings, "CELERY_TO_ENCODE", False)
1✔
43
EMAIL_ON_ENCODING_COMPLETION = getattr(settings, "EMAIL_ON_ENCODING_COMPLETION", True)
1✔
44

45
USE_REMOTE_ENCODING_TRANSCODING = getattr(
1✔
46
    settings, "USE_REMOTE_ENCODING_TRANSCODING", False
47
)
48
if USE_REMOTE_ENCODING_TRANSCODING:
1✔
49
    from .encoding_tasks import start_encoding_task, start_studio_task
1✔
50

51
FFMPEG_DRESSING_INPUT = getattr(
1✔
52
    settings, "FFMPEG_DRESSING_INPUT", FFMPEG_DRESSING_INPUT
53
)
54

55
USE_RUNNER_MANAGER = getattr(settings, "USE_RUNNER_MANAGER", False)
1✔
56

57
# ##########################################################################
58
# ENCODE VIDEO: THREAD TO LAUNCH ENCODE
59
# ##########################################################################
60

61

62
def start_encode(video_id: int, threaded=True) -> None:
1✔
63
    """Start video encoding."""
64
    # Special case for runner manager: delegate encoding to a remote service and return immediately without threading logic
65
    if USE_RUNNER_MANAGER:
1✔
NEW
66
        log.info("Start encode, with runner manager, for id: %s" % video_id)
×
67
        # Load module here to prevent circular import
NEW
68
        from .runner_manager import encode_video as runner_encode_video
×
69

NEW
70
        runner_encode_video(video_id)
×
71
    else:
72
        log.info("Start encode, without runner manager, for id: %s" % video_id)
1✔
73
        if threaded:
1✔
74
            if CELERY_TO_ENCODE:
1✔
NEW
75
                task_start_encode.delay(video_id)
×
76
            else:
77
                log.info("START ENCODE VIDEO ID %s" % video_id)
1✔
78
                t = threading.Thread(target=encode_video, args=[video_id])
1✔
79
                t.daemon = True
1✔
80
                t.start()
1✔
81
        else:
82
            encode_video(video_id)
1✔
83

84

85
def start_encode_studio(
1✔
86
    recording_id, video_output, videos, subtime, presenter, threaded=True
87
) -> None:
88
    """Start studio encoding."""
89
    # Special case for runner manager: delegate encoding to a remote service and return immediately without threading logic
NEW
90
    if USE_RUNNER_MANAGER:
×
NEW
91
        log.info("Start encode studio, with runner manager, for id: %s" % recording_id)
×
92
        # Load module here to prevent circular import
NEW
93
        from .runner_manager import encode_studio_recording
×
94

NEW
95
        encode_studio_recording(recording_id)
×
96
    else:
NEW
97
        log.info(
×
98
            "Start encode studio, without runner manager, for id: %s" % recording_id
99
        )
NEW
100
        if threaded:
×
NEW
101
            if CELERY_TO_ENCODE:
×
NEW
102
                task_start_encode_studio.delay(
×
103
                    recording_id, video_output, videos, subtime, presenter
104
                )
105
            else:
NEW
106
                log.info("START ENCODE VIDEO ID %s" % recording_id)
×
NEW
107
                t = threading.Thread(
×
108
                    target=encode_video_studio,
109
                    args=[recording_id, video_output, videos, subtime, presenter],
110
                )
NEW
111
                t.daemon = True
×
NEW
112
                t.start()
×
113
        else:
NEW
114
            encode_video_studio(recording_id, video_output, videos, subtime, presenter)
×
115

116

117
def encode_video_studio(recording_id, video_output, videos, subtime, presenter) -> None:
1✔
118
    """ENCODE STUDIO: MAIN FUNCTION."""
119
    msg = ""
×
120
    if USE_REMOTE_ENCODING_TRANSCODING:
×
121
        start_studio_task.delay(recording_id, video_output, videos, subtime, presenter)
×
122
    else:
123
        msg = start_encode_video_studio(video_output, videos, subtime, presenter)
×
124
        store_encoding_studio_info(recording_id, video_output, msg)
×
125

126

127
def store_encoding_studio_info(recording_id, video_output, msg) -> None:
1✔
128
    recording = Recording.objects.get(id=recording_id)
×
129
    recording.comment += msg
×
130
    recording.save()
×
131
    if check_file(video_output):
×
132
        from pod.recorder.plugins.type_studio import save_basic_video
×
133

134
        video = save_basic_video(recording, video_output)
×
135
        encode_video(video.id)
×
136
    else:
137
        msg = "Wrong file or path:\n%s" % video_output
×
138
        send_email_recording(msg, recording_id)
×
139

140

141
def encode_video(video_id: int) -> None:
1✔
142
    """ENCODE VIDEO: MAIN FUNCTION."""
143
    start = "Start at: %s" % time.ctime()
1✔
144

145
    video_to_encode = Video.objects.get(id=video_id)
1✔
146
    video_to_encode.encoding_in_progress = True
1✔
147
    video_to_encode.save()
1✔
148

149
    if not check_file(video_to_encode.video.path):
1✔
150
        msg = "Wrong file or path:\n%s" % video_to_encode.video.path
1✔
151
        add_encoding_log(video_id, msg)
1✔
152
        change_encoding_step(video_id, -1, msg)
1✔
153
        send_email(msg, video_id)
1✔
154
        return
1✔
155

156
    change_encoding_step(video_id, 0, "start")
1✔
157
    # start and stop cut?
158
    encoding_video = get_encoding_video(video_to_encode)
1✔
159
    encoding_video.add_encoding_log("start_time", "", True, start)
1✔
160
    change_encoding_step(video_id, 1, "remove old data")
1✔
161
    encoding_video.remove_old_data()
1✔
162

163
    if USE_REMOTE_ENCODING_TRANSCODING:
1✔
164
        change_encoding_step(video_id, 2, "start remote encoding")
1✔
165
        dressing = None
1✔
166
        dressing_input = ""
1✔
167
        if Dressing.objects.filter(videos=video_to_encode).exists():
1✔
168
            dressing = Dressing.objects.get(videos=video_to_encode)
1✔
169
            if dressing:
1✔
170
                dressing_input = get_dressing_input(dressing, FFMPEG_DRESSING_INPUT)
1✔
171
        start_encoding_task.delay(
1✔
172
            encoding_video.id,
173
            encoding_video.video_file,
174
            encoding_video.cutting_start,
175
            encoding_video.cutting_stop,
176
            json_dressing=dressing.to_json() if dressing else None,
177
            dressing_input=dressing_input,
178
        )
179
    else:
180
        change_encoding_step(video_id, 2, "start standard encoding")
1✔
181
        encoding_video.start_encode()
1✔
182
        final_video = store_encoding_info(video_id, encoding_video)
1✔
183

184
        if encoding_video.error_encoding:
1✔
185
            enc_log, created = EncodingLog.objects.get_or_create(video=final_video)
×
186
            msg = "Error during video `%s` encoding." % video_id
×
187
            if created is False:
×
188
                msg += " See log at:\n%s" % enc_log.logfile.url
×
189
            change_encoding_step(video_id, -1, msg)
×
190
            send_email(msg, video_id)
×
191
        else:
192
            end_of_encoding(final_video)
1✔
193

194

195
def store_encoding_info(video_id: int, encoding_video: Encoding_video_model) -> Video:
1✔
196
    """Store all encoding file and informations from encoding tasks."""
197
    change_encoding_step(video_id, 3, "store encoding info")
1✔
198
    final_video = encoding_video.store_json_info()
1✔
199
    final_video.is_video = final_video.get_video_m4a() is None
1✔
200
    final_video.encoding_in_progress = False
1✔
201
    final_video.save()
1✔
202
    return final_video
1✔
203

204

205
def get_encoding_video(video_to_encode: Video) -> Encoding_video_model:
1✔
206
    """Get the encoding video object from video."""
207
    dressing = None
1✔
208
    dressing_input = ""
1✔
209
    if Dressing.objects.filter(videos=video_to_encode).exists():
1✔
210
        dressing = Dressing.objects.get(videos=video_to_encode)
1✔
211
        if dressing:
1✔
212
            dressing_input = get_dressing_input(dressing, FFMPEG_DRESSING_INPUT)
1✔
213

214
    if CutVideo.objects.filter(video=video_to_encode).exists():
1✔
215
        cut = CutVideo.objects.get(video=video_to_encode)
1✔
216
        cut_start = time_to_seconds(cut.start)
1✔
217
        cut_end = time_to_seconds(cut.end)
1✔
218
        encoding_video = Encoding_video_model(
1✔
219
            video_to_encode.id,
220
            video_to_encode.video.path,
221
            cut_start,
222
            cut_end,
223
            json_dressing=dressing.to_json() if dressing else None,
224
            dressing_input=dressing_input,
225
        )
226
        return encoding_video
1✔
227

228
    return Encoding_video_model(
1✔
229
        video_to_encode.id,
230
        video_to_encode.video.path,
231
        0,
232
        0,
233
        json_dressing=dressing.to_json() if dressing else None,
234
        dressing_input=dressing_input,
235
    )
236

237

238
def end_of_encoding(video: Video) -> None:
1✔
239
    """Notify user at the end of encoding & call transcription."""
240
    if (
1✔
241
        USE_NOTIFICATIONS
242
        and video.owner.owner.accepts_notifications
243
        and PushInformation.objects.filter(user=video.owner).exists()
244
    ):
245
        send_notification_encoding(video)
×
246
    if EMAIL_ON_ENCODING_COMPLETION:
1✔
247
        send_email_encoding(video)
1✔
248

249
    transcript_video(video.id)
1✔
250
    change_encoding_step(video.id, 0, "end of encoding")
1✔
251

252

253
def transcript_video(video_id: int) -> None:
1✔
254
    """Transcript video audio to text."""
255
    video = Video.objects.get(id=video_id)
1✔
256
    if USE_TRANSCRIPTION and video.transcript not in ["", "0", "1"]:
1✔
257
        change_encoding_step(video_id, 4, "transcript video")
×
258
        start_transcript_video = getattr(transcript, TRANSCRIPT_VIDEO)
×
259
        start_transcript_video(video_id, False)
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc