Skip to content

Commit

Permalink
Merge pull request #27 from escaped/feat/signals
Browse files Browse the repository at this point in the history
Add signals, fixes #12
  • Loading branch information
escaped committed Jan 2, 2021
2 parents 7109135 + fa2ab42 commit 2b6272c
Show file tree
Hide file tree
Showing 8 changed files with 316 additions and 3 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

* support for django-storages, @lifenautjoe & @bashu
* add signals for encoding

### Changed

Expand Down
73 changes: 73 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,79 @@ def create_thumbnail(sender, instance, **kwargs):
enqueue(tasks.create_thumbnail, instance.pk)
```

### Signals

During the encoding multiple signals are emitted to report the progress.
You can register to the signals as described in the [Django documentation](https://docs.djangoproject.com/en/3.1/topics/signals/#connecting-to-signals-sent-by-specific-senders).

This simple example demonstrates, on how to update the "video model" once the convertion is finished.

```python
# apps.py
from django.apps import AppConfig


class MyAppConfig(AppConfig):
# ...

def ready(self) -> None:
from . import signals # register signals


# signals.py
from typing import Type

from django.dispatch import receiver
from video_encoding import signals

from myapp.models import Video


@receiver(signals.encoding_finished, sender=Video)
def mark_as_finished(sender: Type[Video], instance: Video) -> None:
"""
Mark video as "convertion has been finished".
"""
video.processed = True
video.save(update_fields=['processed'])
```

#### `signals.encoding_started`

This is sent before the encoding starts.

_Arguments_
`sender: Type[models.Model]`: Model which contains the `VideoField`.
`instance: models.Model)`: Instance of the model containing the `VideoField`.

#### `signals.encoding_finished`

Like `encoding_started()`, but sent after the file had been converted into all formats.

_Arguments_
`sender: Type[models.Model]`: Model which contains the `VideoField`.
`instance: models.Model)`: Instance of the model containing the `VideoField`.

#### `signals.format_started`

This is sent before the video is converted to one of the configured formats.

_Arguments_
`sender: Type[models.Model]`: Model which contains the `VideoField`.
`instance: models.Model)`: Instance of the model containing the `VideoField`.
`format: Format`: The format instance, which will reference the encoded video file.

#### `signals.format_finished`

Like `format_finished`, but sent after the video encoding process and includes whether the encoding was succesful or not.

_Arguments_
`sender: Type[models.Model]`: Model which contains the `VideoField`.
`instance: models.Model)`: Instance of the model containing the `VideoField`.
`format: Format`: The format instance, which will reference the encoded video file.
`result: ConversionResult`: Instance of `video_encoding.signals.ConversionResult` and indicates whether the convertion `FAILED`, `SUCCEEDED` or was `SKIPPED`.


## Configuration

**VIDEO_ENCODING_THREADS** (default: `1`)
Expand Down
14 changes: 13 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ pytest-django = "^3.9.0"
pytest-mock = "^3.3.1"
tox = "^3.20.0"
tox-gh-actions = "^1.3.0"
matchlib = "^0.2.1"

[tool.black]
line-length = 88
Expand Down
17 changes: 16 additions & 1 deletion test_proj/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
from typing import IO, Any, Generator

import pytest
from django.contrib.contenttypes.models import ContentType
from django.core.files import File
from django.core.files.storage import FileSystemStorage

from test_proj.media_library.models import Video
from test_proj.media_library.models import Format, Video
from video_encoding.backends.ffmpeg import FFmpegBackend


Expand Down Expand Up @@ -50,6 +51,20 @@ def local_video(video_path) -> Generator[Video, None, None]:
video.delete()


@pytest.fixture
def format(video_path, local_video) -> Generator[Format, None, None]:
format = Format.objects.create(
object_id=local_video.pk,
content_type=ContentType.objects.get_for_model(local_video),
field_name='file',
format='mp4_hd',
progress=100,
)
#
format.file.save('test.MTS', File(open(video_path, 'rb')), save=True)
yield format


@pytest.fixture
def remote_video(local_video) -> Generator[Video, None, None]:
"""
Expand Down
174 changes: 174 additions & 0 deletions test_proj/media_library/tests/test_signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import pytest
from matchlib import matches

from video_encoding import signals, tasks
from video_encoding.exceptions import VideoEncodingError

from .. import models


@pytest.mark.django_db
def test_signals(monkeypatch, mocker, local_video: models.Video) -> None:
"""
Make sure encoding signals are send.
There are currently 4 signals:
- encoding_started
- format_started
- format_finished
- encoding_finished
"""
# encode only to one format
encoding_format = tasks.settings.VIDEO_ENCODING_FORMATS['FFmpeg'][0]
monkeypatch.setattr(
tasks.settings, 'VIDEO_ENCODING_FORMATS', {'FFmpeg': [encoding_format]}
)

mocker.patch.object(tasks, '_encode') # don't encode anything

listener = mocker.MagicMock()
signals.encoding_started.connect(listener)
signals.format_started.connect(listener)
signals.format_finished.connect(listener)
signals.encoding_finished.connect(listener)

tasks.convert_video(local_video.file)

assert listener.call_count == 4
# check arguments and make sure they are called in the right order
# encoding_started
_, kwargs = listener.call_args_list[0]
assert kwargs == {
'signal': signals.encoding_started,
'sender': models.Video,
'instance': local_video,
}

# format started
_, kwargs = listener.call_args_list[1]
assert matches(
kwargs,
{
'signal': signals.format_started,
'sender': models.Format,
'instance': local_video,
'format': ...,
},
)
assert isinstance(kwargs['format'], models.Format)
assert kwargs['format'].format == encoding_format['name']
assert kwargs['format'].progress == 0

# format finished
_, kwargs = listener.call_args_list[2]
assert matches(
kwargs,
{
'signal': signals.format_finished,
'sender': models.Format,
'instance': local_video,
'format': ...,
'result': signals.ConversionResult.SUCCEEDED,
},
)
assert isinstance(kwargs['format'], models.Format)
assert kwargs['format'].format == encoding_format['name']

# encoding finished
_, kwargs = listener.call_args_list[3]
assert kwargs == {
'signal': signals.encoding_finished,
'sender': models.Video,
'instance': local_video,
}


@pytest.mark.django_db
def test_signals__encoding_failed(
monkeypatch, mocker, local_video: models.Video
) -> None:
"""
Make sure encoding signal reports failed, if the encoding was not succesful.
"""
# encode only to one format
encoding_format = tasks.settings.VIDEO_ENCODING_FORMATS['FFmpeg'][0]
monkeypatch.setattr(
tasks.settings, 'VIDEO_ENCODING_FORMATS', {'FFmpeg': [encoding_format]}
)

mocker.patch.object(
tasks, '_encode', side_effect=VideoEncodingError()
) # encoding should fail

listener = mocker.MagicMock()
signals.format_started.connect(listener)
signals.format_finished.connect(listener)

tasks.convert_video(local_video.file)

assert listener.call_count == 2
# check arguments and make sure they are called in the right order
# format started
_, kwargs = listener.call_args_list[0]
assert matches(kwargs, {'signal': signals.format_started, ...: ...})

# format finished, but failed
_, kwargs = listener.call_args_list[1]
assert matches(
kwargs,
{
'signal': signals.format_finished,
'sender': models.Format,
'instance': local_video,
'format': ...,
'result': signals.ConversionResult.FAILED,
},
)
assert isinstance(kwargs['format'], models.Format)
assert kwargs['format'].format == encoding_format['name']


@pytest.mark.django_db
def test_signals__encoding_skipped(
monkeypatch, mocker, local_video: models.Video, format: models.Format
) -> None:
"""
Make sure encoding signal reports skipped, if file had been encoded before.
"""
# encode only to one format
encoding_format = tasks.settings.VIDEO_ENCODING_FORMATS['FFmpeg'][0]
monkeypatch.setattr(
tasks.settings, 'VIDEO_ENCODING_FORMATS', {'FFmpeg': [encoding_format]}
)

mocker.patch.object(tasks, '_encode') # don't encode anything
# encoding has already been done for the given format
format.format = encoding_format["name"]
format.save()

listener = mocker.MagicMock()
signals.format_started.connect(listener)
signals.format_finished.connect(listener)

tasks.convert_video(local_video.file)

assert listener.call_count == 2
# check arguments and make sure they are called in the right order
# format started
_, kwargs = listener.call_args_list[0]
assert matches(kwargs, {'signal': signals.format_started, ...: ...})

# format finished, but skipped
_, kwargs = listener.call_args_list[1]
assert matches(
kwargs,
{
'signal': signals.format_finished,
'sender': models.Format,
'instance': local_video,
'format': ...,
'result': signals.ConversionResult.SKIPPED,
},
)
assert isinstance(kwargs['format'], models.Format)
assert kwargs['format'].format == encoding_format['name']
16 changes: 16 additions & 0 deletions video_encoding/signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import enum

from django.dispatch import Signal


class ConversionResult(enum.Enum):
SUCCEEDED = "success"
FAILED = "failed"
SKIPPED = "skipped"


encoding_started = Signal()
encoding_finished = Signal()

format_started = Signal()
format_finished = Signal()
Loading

0 comments on commit 2b6272c

Please sign in to comment.