├── .circleci └── config.yml ├── .credo.exs ├── .formatter.exs ├── .github ├── CODEOWNERS ├── actions │ ├── add_pr_to_smackore_board │ │ └── action.yml │ └── close_issue │ │ └── action.yml └── workflows │ ├── enforce-changelog-update.yml │ ├── on_pr_opened.yaml │ └── update-packages-list.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── assets ├── drawio_schemes │ ├── spec_with_audio.drawio │ └── spec_without_audio.drawio └── images │ ├── observer_graph.png │ ├── spec_with_audio.svg │ └── spec_without_audio.svg ├── benchmark ├── compare.exs ├── metric.ex ├── metric │ ├── in_progress_memory.ex │ ├── message_queues_length.ex │ └── time.ex ├── run.exs └── run │ ├── branched_filter.ex │ ├── linear_filter.ex │ ├── pipeline.ex │ └── reductions.ex ├── config └── config.exs ├── example.livemd ├── guides ├── components_lifecycle.md ├── timer.md └── upgrading │ ├── v0.11.md │ ├── v0.12.md │ ├── v1.0.0-rc0.md │ ├── v1.0.0-rc1.md │ └── v1.0.0.md ├── lib └── membrane │ ├── bin.ex │ ├── bin │ ├── action.ex │ ├── callback_context.ex │ └── pad_data.ex │ ├── buffer.ex │ ├── buffer │ ├── metric.ex │ └── metric │ │ ├── byte_size.ex │ │ └── count.ex │ ├── child.ex │ ├── child_entry.ex │ ├── children_spec.ex │ ├── clock.ex │ ├── component_path.ex │ ├── connector.ex │ ├── core │ ├── bin.ex │ ├── bin │ │ ├── action_handler.ex │ │ ├── callback_context.ex │ │ ├── pad_controller.ex │ │ ├── state.ex │ │ └── zombie.ex │ ├── callback_handler.ex │ ├── child.ex │ ├── child │ │ ├── lifecycle_controller.ex │ │ ├── pad_controller.ex │ │ ├── pad_model.ex │ │ ├── pad_spec_handler.ex │ │ └── pads_specs.ex │ ├── component.ex │ ├── docs_helper.ex │ ├── element.ex │ ├── element │ │ ├── action_handler.ex │ │ ├── atomic_demand.ex │ │ ├── atomic_demand │ │ │ ├── atomic_flow_status.ex │ │ │ ├── distributed_atomic.ex │ │ │ └── distributed_atomic │ │ │ │ └── worker.ex │ │ ├── auto_flow_controller.ex │ │ ├── buffer_controller.ex │ │ ├── callback_context.ex │ │ ├── demand_controller.ex │ │ ├── diamond_detection_controller.ex │ │ ├── diamond_detection_controller │ │ │ ├── diamond_detection_state.ex │ │ │ ├── diamond_logger.ex │ │ │ └── path_in_grapth.ex │ │ ├── effective_flow_controller.ex │ │ ├── event_controller.ex │ │ ├── lifecycle_controller.ex │ │ ├── manual_flow_controller.ex │ │ ├── manual_flow_controller │ │ │ └── input_queue.ex │ │ ├── pad_controller.ex │ │ ├── playback_queue.ex │ │ ├── state.ex │ │ └── stream_format_controller.ex │ ├── events │ │ ├── end_of_stream.ex │ │ └── start_of_stream.ex │ ├── filter_aggregator │ │ ├── action.ex │ │ └── context.ex │ ├── helper │ │ └── fast_map.ex │ ├── inspect.ex │ ├── legacy_telemetry.ex │ ├── lifecycle_controller.ex │ ├── message.ex │ ├── options_specs.ex │ ├── parent.ex │ ├── parent │ │ ├── child_entry_parser.ex │ │ ├── child_life_controller.ex │ │ ├── child_life_controller │ │ │ ├── crash_group_utils.ex │ │ │ ├── link_utils.ex │ │ │ └── startup_utils.ex │ │ ├── children_model.ex │ │ ├── clock_handler.ex │ │ ├── crash_group.ex │ │ ├── diamond_detection_controller.ex │ │ ├── lifecycle_controller.ex │ │ ├── link.ex │ │ ├── link_endpoint.ex │ │ └── specification_parser.ex │ ├── pipeline.ex │ ├── pipeline │ │ ├── action_handler.ex │ │ ├── callback_context.ex │ │ ├── state.ex │ │ ├── supervisor.ex │ │ └── zombie.ex │ ├── process_helper.ex │ ├── stalker.ex │ ├── subprocess_supervisor.ex │ ├── telemetry.ex │ ├── timer.ex │ ├── timer_controller.ex │ └── utils.ex │ ├── debug │ ├── filter.ex │ └── sink.ex │ ├── element.ex │ ├── element │ ├── action.ex │ ├── base.ex │ ├── callback_context.ex │ ├── pad_data.ex │ ├── with_input_pads.ex │ └── with_output_pads.ex │ ├── endpoint.ex │ ├── event.ex │ ├── event │ ├── discontinuity.ex │ └── underrun.ex │ ├── event_protocol.ex │ ├── exceptions.ex │ ├── fake_sink.ex │ ├── filter.ex │ ├── filter_aggregator.ex │ ├── funnel.ex │ ├── funnel │ └── new_input_event.ex │ ├── keyframe_request_event.ex │ ├── logger.ex │ ├── notification.ex │ ├── pad.ex │ ├── payload.ex │ ├── pipeline.ex │ ├── pipeline │ ├── action.ex │ └── callback_context.ex │ ├── playback.ex │ ├── rc_message.ex │ ├── rc_pipeline.ex │ ├── remote_stream.ex │ ├── resource_guard.ex │ ├── sink.ex │ ├── source.ex │ ├── stream_format.ex │ ├── sync.ex │ ├── tee.ex │ ├── telemetry.ex │ ├── testing │ ├── assertions.ex │ ├── dynamic_source.ex │ ├── endpoint.ex │ ├── event.ex │ ├── mock_resource_guard.ex │ ├── notification.ex │ ├── pipeline.ex │ ├── sink.ex │ └── source.ex │ ├── time.ex │ └── utility_supervisor.ex ├── mix.exs ├── mix.lock ├── priv └── plts │ └── .gitkeep ├── scripts ├── elixir │ └── update_packages_list.exs └── python │ ├── get_author_origin.py │ └── get_ticket_id.py └── test ├── membrane ├── buffer_metric │ ├── byte_size_test.exs │ └── count_test.exs ├── buffer_test.exs ├── clock_test.exs ├── core │ ├── element │ │ ├── action_handler_test.exs │ │ ├── atomic_demand_test.exs │ │ ├── event_controller_test.exs │ │ ├── input_queue_test.exs │ │ ├── lifecycle_controller_test.exs │ │ ├── pad_controller_test.exs │ │ ├── pad_model_test.exs │ │ └── stream_format_controller_test.exs │ ├── element_test.exs │ ├── helper │ │ └── fast_map_test.exs │ ├── message_test.exs │ ├── parent │ │ └── structure_parser_test.exs │ └── pipeline_test.exs ├── diamond_detection_test.exs ├── element_test.exs ├── filter_aggregator │ ├── integration_test.exs │ ├── internal_action_test.exs │ └── unit_test.exs ├── integration │ ├── actions_handling_order_test.exs │ ├── auto_demands_test.exs │ ├── bin_test.exs │ ├── callbacks_test.exs │ ├── child_crash_test.exs │ ├── child_life_callbacks_test.exs │ ├── child_pad_removed_test.exs │ ├── child_removal_test.exs │ ├── child_spawn_test.exs │ ├── connector_test.exs │ ├── debug_elements_test.exs │ ├── defer_setup_test.exs │ ├── delayed_demands_loop_test.exs │ ├── demands_test.exs │ ├── distributed_pipeline_test.exs │ ├── effective_flow_control_resolution_test.exs │ ├── elements_compatibility_test.exs │ ├── end_of_stream_test.exs │ ├── endpoint_test.exs │ ├── funnel_test.exs │ ├── linking_test.exs │ ├── links_validation_test.exs │ ├── no_stream_format_crash_test.exs │ ├── stalker_test.exs │ ├── stream_format_test.exs │ ├── sync_test.exs │ ├── sync_test │ │ └── ticking_pace.exs │ ├── synchronous_pipeline_call_test.exs │ ├── tee_test.exs │ ├── timer_test.exs │ └── toilet_forwarding_test.exs ├── log_metadata_test.exs ├── pad_test.exs ├── pipeline_supervisor_test.exs ├── pipeline_test.exs ├── remote_controlled │ └── pipeline_test.exs ├── resource_guard_test.exs ├── sync_test.exs ├── telemetry_test.exs ├── testing │ ├── dynamic_source_test.exs │ ├── endpoint_test.exs │ ├── mock_resource_guard_test.exs │ ├── pipeline_assertions_test.exs │ ├── pipeline_test.exs │ ├── sink_test.exs │ └── source_test.exs ├── time_test.exs └── utility_supervisor_test.exs ├── support ├── accepted_format_test │ ├── inner_sink_bin.ex │ ├── inner_source_bin.ex │ ├── outer_sink_bin.ex │ ├── outer_source_bin.ex │ ├── restrictive_sink.ex │ ├── restrictive_source.ex │ ├── sink.ex │ ├── source.ex │ └── stream.ex ├── bin │ └── test_bins.ex ├── child_crash_test │ ├── filter.ex │ └── pipeline.ex ├── child_removal_test │ ├── child_removing_pipeline.ex │ ├── filter.ex │ ├── filter_to_be_removed.ex │ ├── pipeline.ex │ └── source_notyfing_when_pad_removed.ex ├── demands_test │ ├── filter.ex │ └── pipeline.ex ├── distributed.ex ├── dynamic_filter.ex ├── element │ ├── trivial_filter.ex │ ├── trivial_sink.ex │ └── trivial_source.ex ├── log_metadata_test │ └── pipeline.ex ├── stream_format_mock.ex ├── sync │ ├── helper.ex │ ├── pipeline.ex │ ├── sink.ex │ ├── source.ex │ └── sync_bin.ex └── trivial_pipeline.ex └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | locals_without_parens = 2 | [ 3 | def_output_pad: 2, 4 | def_input_pad: 2, 5 | def_options: 1, 6 | def_clock: 1, 7 | def_type_from_list: 1, 8 | assert_receive_message: 3 9 | ] 10 | 11 | [ 12 | inputs: [ 13 | "{lib,test,config,benchmark,scripts}/**/*.{ex,exs}", 14 | "*.exs" 15 | ], 16 | locals_without_parens: locals_without_parens, 17 | export: [ 18 | locals_without_parens: locals_without_parens 19 | ] 20 | ] 21 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @mat-hek 2 | -------------------------------------------------------------------------------- /.github/actions/add_pr_to_smackore_board/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Add PR to Smackore board, if author is from community' 2 | description: 'Adds PR to "New issues by community" column in Smackore project board, if PR author is from outside Membrane Team.' 3 | inputs: 4 | GITHUB_TOKEN: 5 | description: 'GitHub token' 6 | required: true 7 | AUTHOR_LOGIN: 8 | description: 'PR author login' 9 | required: true 10 | PR_URL: 11 | description: 'PR URL' 12 | required: true 13 | runs: 14 | using: 'composite' 15 | steps: 16 | - name: Checkout membrane_core code 17 | uses: actions/checkout@v3 18 | with: 19 | repository: membraneframework/membrane_core 20 | - name: Maybe add PR to board and set ticket status 21 | run: | 22 | # currently this may cause github action crash, more info here: https://github.com/membraneframework/membrane_core/issues/749 23 | 24 | export PROJECT_NUMBER=19 25 | export PROJECT_ID=PVT_kwDOAYE_z84AWEIB 26 | export STATUS_FIELD_ID=PVTSSF_lADOAYE_z84AWEIBzgOGd1k 27 | export TARGET_COLUMN_ID=e6b1ee10 28 | 29 | export AUTHOR_ORIGIN=$(gh api -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" /orgs/membraneframework/teams/membraneteam/members | python scripts/python/get_author_origin.py $AUTHOR_LOGIN) 30 | 31 | if [ "$AUTHOR_ORIGIN" == "COMMUNITY" ] 32 | then 33 | gh pr edit "$PR_URL" --add-project Smackore 34 | sleep 10 35 | 36 | export TICKET_ID=$(gh project item-list $PROJECT_NUMBER --owner membraneframework --format json --limit 10000000 | python scripts/python/get_ticket_id.py "$PR_URL") 37 | gh project item-edit --id $TICKET_ID --field-id $STATUS_FIELD_ID --project-id $PROJECT_ID --single-select-option-id $TARGET_COLUMN_ID 38 | fi 39 | 40 | env: 41 | GH_TOKEN: ${{ inputs.GITHUB_TOKEN }} 42 | AUTHOR_LOGIN: ${{ inputs.AUTHOR_LOGIN }} 43 | PR_URL: ${{ inputs.PR_URL }} 44 | shell: bash 45 | -------------------------------------------------------------------------------- /.github/actions/close_issue/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Close issue when opened' 2 | description: 'Closes a newly opened issue, tags it and puts it in the proper column in Smackore project board.' 3 | inputs: 4 | GITHUB_TOKEN: 5 | description: 'GitHub token' 6 | required: true 7 | ISSUE_URL: 8 | description: 'Issue URL' 9 | required: true 10 | ISSUE_NUMBER: 11 | description: 'Issue number' 12 | required: true 13 | REPOSITORY: 14 | description: 'Repository' 15 | required: true 16 | runs: 17 | using: 'composite' 18 | steps: 19 | - name: Setup Elixir 20 | uses: erlef/setup-beam@v1 21 | with: 22 | otp-version: '26.1' 23 | elixir-version: '1.15.6' 24 | - name: Checkout code 25 | uses: actions/checkout@v3 26 | with: 27 | repository: membraneframework/membrane_core 28 | - name: Create ticket and close issue 29 | run: | 30 | export PROJECT_NUMBER=19 31 | export PROJECT_ID=PVT_kwDOAYE_z84AWEIB 32 | export STATUS_FIELD_ID=PVTSSF_lADOAYE_z84AWEIBzgOGd1k 33 | export TARGET_COLUMN_ID=fa223107 34 | export CORE_URL=https://github.com/membraneframework/membrane_core 35 | export ISSUE_CLOSE_COMMENT="Issues related to $REPOSITORY are stored in [membrane_core]($CORE_URL), so we close this issue here and we encourage you to open it [there]($CORE_URL)." 36 | 37 | gh issue edit $ISSUE_URL --add-project "Smackore" 38 | sleep 10 39 | 40 | export TICKET_ID=$(gh project item-list $PROJECT_NUMBER --owner membraneframework --format json --limit 10000000 | python ./scripts/python/get_ticket_id.py "$ISSUE_URL") 41 | gh issue close $ISSUE_URL --comment "$ISSUE_CLOSE_COMMENT" --reason "not planned" 42 | sleep 10 43 | 44 | gh project item-edit --id $TICKET_ID --field-id $STATUS_FIELD_ID --project-id $PROJECT_ID --single-select-option-id $TARGET_COLUMN_ID 45 | env: 46 | GH_TOKEN: ${{ inputs.GITHUB_TOKEN }} 47 | ISSUE_URL: ${{ inputs.ISSUE_URL }} 48 | ISSUE_NUMBER: ${{ inputs.ISSUE_NUMBER }} 49 | REPOSITORY: ${{ inputs.REPOSITORY }} 50 | shell: bash 51 | -------------------------------------------------------------------------------- /.github/workflows/enforce-changelog-update.yml: -------------------------------------------------------------------------------- 1 | name: Enforce CHANGELOG Update 2 | on: 3 | pull_request: 4 | branches: 5 | - master 6 | workflow_call: 7 | jobs: 8 | check-changelog-update: 9 | if: ${{ !contains( github.event.pull_request.labels.*.name, 'no-changelog') }} 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout repository 13 | uses: actions/checkout@v2 14 | - id: files 15 | uses: jitterbit/get-changed-files@v1 16 | - name: Verify changes in CHANGELOG file 17 | id: verify-changes 18 | run: | 19 | changelogPresent=0 20 | for changed_file in ${{ steps.files.outputs.all }}; do 21 | if [ "$changed_file" == "CHANGELOG.md" ]; then 22 | changelogPresent=1 23 | break 24 | fi 25 | done 26 | echo "::set-output name=changelog-updated::$changelogPresent" 27 | - name: Check 28 | if: ${{ steps.verify-changes.outputs.changelog-updated == 0 }} 29 | uses: actions/github-script@v3 30 | with: 31 | script: | 32 | core.setFailed('Add label "no-changelog" or update CHANGELOG.md on pull request to master.') 33 | -------------------------------------------------------------------------------- /.github/workflows/on_pr_opened.yaml: -------------------------------------------------------------------------------- 1 | name: Add PR to Smackore project board, if the author is from outside Membrane Team 2 | on: 3 | pull_request_target: 4 | types: 5 | - opened 6 | jobs: 7 | maybe_add_to_project_board: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout membrane_core 11 | uses: actions/checkout@v3 12 | with: 13 | repository: membraneframework/membrane_core 14 | - name: Puts PR in "New PRs by community" column in the Smackore project, if the author is from outside Membrane Team 15 | uses: ./.github/actions/add_pr_to_smackore_board 16 | with: 17 | GITHUB_TOKEN: ${{ secrets.MEMBRANEFRAMEWORKADMIN_TOKEN }} 18 | AUTHOR_LOGIN: ${{ github.event.pull_request.user.login }} 19 | PR_URL: ${{ github.event.pull_request.html_url }} 20 | -------------------------------------------------------------------------------- /.github/workflows/update-packages-list.yml: -------------------------------------------------------------------------------- 1 | name: Update packages list 2 | on: 3 | schedule: 4 | - cron: '0 0 1 1-12/1 *' # Run every month 5 | workflow_dispatch: {} 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | name: Update packages list in README 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: erlef/setup-beam@v1 14 | with: 15 | otp-version: '27.0' 16 | elixir-version: '1.17.2' 17 | - name: Update packages list 18 | env: 19 | GH_TOKEN: ${{ secrets.BOT_TOKEN }} 20 | run: | 21 | echo "Run script" 22 | elixir scripts/elixir/update_packages_list.exs > output.txt 2> output.txt || ERROR=$? || true 23 | cat output.txt 24 | echo "Set status" 25 | STATUS=$(if [ -n "$ERROR" ]; then echo "[Failed]"; else echo "[Passed]"; fi) 26 | echo "Status: ${STATUS}" 27 | echo "Configure git" 28 | git config user.name 'Membrane Bot' 29 | git config user.email 'bot@membrane.stream' 30 | echo "Checkout" 31 | git checkout -B auto-update-packages-list 32 | echo "Commit" 33 | git add README.md 34 | git commit -m "auto update packages list in readme" --allow-empty 35 | echo "Push" 36 | git push -f -u origin auto-update-packages-list 37 | echo "Create PR" 38 | gh pr create -B master -H auto-update-packages-list --label no-changelog --title "${STATUS} Auto update packages list" --body-file output.txt \ 39 | || gh pr edit auto-update-packages-list --title "${STATUS} Auto update packages list" --body-file output.txt -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .elixir_ls 2 | /priv/plts/* 3 | !/priv/plts/.gitkeep 4 | 5 | # Created by https://www.gitignore.io/api/c,vim,linux,macos,elixir,windows,visualstudiocode 6 | 7 | ### C ### 8 | # Prerequisites 9 | *.d 10 | 11 | # Object files 12 | *.o 13 | *.ko 14 | *.obj 15 | *.elf 16 | 17 | # Linker output 18 | *.ilk 19 | *.map 20 | *.exp 21 | 22 | # Precompiled Headers 23 | *.gch 24 | *.pch 25 | 26 | # Libraries 27 | *.lib 28 | *.a 29 | *.la 30 | *.lo 31 | 32 | # Shared objects (inc. Windows DLLs) 33 | *.dll 34 | *.so 35 | *.so.* 36 | *.dylib 37 | 38 | # Executables 39 | *.exe 40 | *.out 41 | *.app 42 | *.i*86 43 | *.x86_64 44 | *.hex 45 | 46 | # Debug files 47 | *.dSYM/ 48 | *.su 49 | *.idb 50 | *.pdb 51 | 52 | # Kernel Module Compile Results 53 | *.mod* 54 | *.cmd 55 | .tmp_versions/ 56 | modules.order 57 | Module.symvers 58 | Mkfile.old 59 | dkms.conf 60 | 61 | ### Elixir ### 62 | /_build 63 | /cover 64 | /deps 65 | /doc 66 | /.fetch 67 | erl_crash.dump 68 | *.ez 69 | *.beam 70 | 71 | ### Elixir Patch ### 72 | ### Linux ### 73 | *~ 74 | 75 | # temporary files which can be created if a process still has a handle open of a deleted file 76 | .fuse_hidden* 77 | 78 | # KDE directory preferences 79 | .directory 80 | 81 | # Linux trash folder which might appear on any partition or disk 82 | .Trash-* 83 | 84 | # .nfs files are created when an open file is removed but is still being accessed 85 | .nfs* 86 | 87 | ### macOS ### 88 | *.DS_Store 89 | .AppleDouble 90 | .LSOverride 91 | 92 | # Icon must end with two \r 93 | Icon 94 | 95 | # Thumbnails 96 | ._* 97 | 98 | # Files that might appear in the root of a volume 99 | .DocumentRevisions-V100 100 | .fseventsd 101 | .Spotlight-V100 102 | .TemporaryItems 103 | .Trashes 104 | .VolumeIcon.icns 105 | .com.apple.timemachine.donotpresent 106 | 107 | # Directories potentially created on remote AFP share 108 | .AppleDB 109 | .AppleDesktop 110 | Network Trash Folder 111 | Temporary Items 112 | .apdisk 113 | 114 | ### Vim ### 115 | # swap 116 | .sw[a-p] 117 | .*.sw[a-p] 118 | # session 119 | Session.vim 120 | # temporary 121 | .netrwhist 122 | # auto-generated tag files 123 | tags 124 | 125 | ### VisualStudioCode ### 126 | .vscode/* 127 | !.vscode/settings.json 128 | !.vscode/tasks.json 129 | !.vscode/launch.json 130 | !.vscode/extensions.json 131 | .history 132 | 133 | ### Windows ### 134 | # Windows thumbnail cache files 135 | Thumbs.db 136 | ehthumbs.db 137 | ehthumbs_vista.db 138 | 139 | # Folder config file 140 | Desktop.ini 141 | 142 | # Recycle Bin used on file shares 143 | $RECYCLE.BIN/ 144 | 145 | # Windows Installer files 146 | *.cab 147 | *.msi 148 | *.msm 149 | *.msp 150 | 151 | # Windows shortcuts 152 | *.lnk 153 | 154 | # End of https://www.gitignore.io/api/c,vim,linux,macos,elixir,windows,visualstudiocode 155 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Any contributions to Membrane Framework are welcome. If you would like contribute, but you're not sure how to start or have some questions, don't hesitate to contact us via channels described in [README](README.md#support-and-questions) 4 | 5 | Before submitting code please read our [Coding style guide](https://membrane.stream/guide/v0.9/contributing_guide.html) 6 | 7 | When contributing to existing repo: 8 | 9 | - fork it 10 | - apply change on some branch 11 | - create a PR from fork to our repo 12 | - await feedback from someone from the team 13 | - after passing the review, the PR will be merged 14 | 15 | If you wish to create a new plugin you can give us a shout and if it's something we want as a part of our ecosystem we'll create a repo for you, guide you on your work and maintain the plugin in future. 16 | 17 | However, if you feel confident enough to maintain it on your own, you can, of course, create your own repo and hex package - we only ask you to follow the naming convetions of the framework package and modules. In that case, don't forget to let us know about your work so we could include it on the list of available plugins. 18 | -------------------------------------------------------------------------------- /assets/images/observer_graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/membraneframework/membrane_core/e40054cffc90a25b5a7b141d47d24e42a728eb15/assets/images/observer_graph.png -------------------------------------------------------------------------------- /benchmark/compare.exs: -------------------------------------------------------------------------------- 1 | # A script providing a functionality to compare results of two performance tests. 2 | 3 | # Comparison of two test results is done with the following command: 4 | # `mix run benchmark/compare.exs ` 5 | # where the "result files" are the files generated with `mix run benchmark/run.exs` script. 6 | # For information about the metric used, see the modules implementing `Benchmark.Metric` behaviour. 7 | 8 | defmodule Benchmark.Compare do 9 | require Logger 10 | 11 | def run(results, ref_results, results_name, ref_results_name) do 12 | if Map.keys(results) != Map.keys(ref_results), 13 | do: raise("Incompatible performance test result files!") 14 | 15 | Enum.each(Map.keys(results), fn test_case -> 16 | test_case_results = Map.get(results, test_case) 17 | test_case_results_ref = Map.get(ref_results, test_case) 18 | 19 | results_str = 20 | Enum.map(Map.keys(test_case_results), fn metric_module -> 21 | """ 22 | METRIC: #{metric_module} 23 | 1. In #{results_name}: 24 | #{inspect(Map.get(test_case_results, metric_module), pretty: true, limit: :infinity)} 25 | 2. In #{ref_results_name}: 26 | #{inspect(Map.get(test_case_results_ref, metric_module), pretty: true, limit: :infinity)} 27 | """ 28 | end) 29 | |> Enum.join() 30 | 31 | Logger.debug(""" 32 | TEST CASE: 33 | #{inspect(test_case, pretty: true, limit: :infinity)} 34 | 35 | #{results_str} 36 | 37 | """) 38 | 39 | Enum.each(Map.keys(test_case_results), fn metric_module -> 40 | metric_value = Map.get(test_case_results, metric_module) 41 | metric_value_ref = Map.get(test_case_results_ref, metric_module) 42 | metric_module.assert(metric_value, metric_value_ref, test_case) 43 | end) 44 | end) 45 | 46 | :ok 47 | end 48 | end 49 | 50 | [results_filename, ref_results_filename] = System.argv() |> Enum.take(2) 51 | results = File.read!(results_filename) |> :erlang.binary_to_term() 52 | ref_results = File.read!(ref_results_filename) |> :erlang.binary_to_term() 53 | results_name = String.split(results_filename, "/") |> Enum.at(-1) 54 | ref_results_name = String.split(ref_results_filename, "/") |> Enum.at(-1) 55 | Benchmark.Compare.run(results, ref_results, results_name, ref_results_name) 56 | -------------------------------------------------------------------------------- /benchmark/metric.ex: -------------------------------------------------------------------------------- 1 | defmodule Benchmark.Metric do 2 | @moduledoc """ 3 | A module defining a behaviour for metrics used in Membrane Core performance benchmarks. 4 | """ 5 | 6 | @typedoc """ 7 | A type describing a single meassurement of a given metric. 8 | 9 | """ 10 | @type meassurement :: any 11 | 12 | @opaque meassurement_state :: any 13 | 14 | @doc """ 15 | A function used to assert that the first meassurement is no worse than the second meassurement. 16 | """ 17 | @callback assert( 18 | first_meassurement :: meassurement(), 19 | second_meassurement :: meassurement(), 20 | additional_params :: any 21 | ) :: 22 | :ok | no_return 23 | 24 | @doc """ 25 | A function aggregating the list of meassurements gathered during multiple runs of the benchamark into a single 26 | meassurement. 27 | 28 | In case the first meassurement is worse than the second, the function should raise. 29 | """ 30 | @callback average([meassurement]) :: meassurement 31 | 32 | @callback start_meassurement(opts :: any) :: meassurement_state 33 | @callback stop_meassurement(meassurement_state) :: meassurement 34 | end 35 | -------------------------------------------------------------------------------- /benchmark/metric/in_progress_memory.ex: -------------------------------------------------------------------------------- 1 | defmodule Benchmark.Metric.InProgressMemory do 2 | @behaviour Benchmark.Metric 3 | 4 | @tolerance_factor 0.5 5 | @sampling_period 100 6 | 7 | @impl true 8 | def assert(memory_samples, memory_samples_ref, test_case) do 9 | cumulative_memory = integrate(memory_samples) 10 | cumulative_memory_ref = integrate(memory_samples_ref) 11 | 12 | if cumulative_memory > cumulative_memory_ref * (1 + @tolerance_factor), 13 | do: 14 | raise( 15 | "The memory performance has got worse! For test case: #{inspect(test_case, pretty: true)} 16 | the cumulative memory used to be: #{cumulative_memory_ref} MB and now it is: #{cumulative_memory} MB" 17 | ) 18 | 19 | :ok 20 | end 21 | 22 | defp integrate(memory_samples) do 23 | Enum.sum(memory_samples) 24 | end 25 | 26 | @impl true 27 | def average(memory_samples_from_multiple_tries) do 28 | memory_samples_from_multiple_tries 29 | |> Enum.zip() 30 | |> Enum.map(&Tuple.to_list(&1)) 31 | |> Enum.map(&(Enum.sum(&1) / (length(&1) * 1_000_000))) 32 | end 33 | 34 | @impl true 35 | def start_meassurement(_opts \\ nil) do 36 | Process.list() |> Enum.each(&:erlang.garbage_collect/1) 37 | 38 | task = 39 | Task.async(fn -> 40 | do_loop() 41 | end) 42 | 43 | task 44 | end 45 | 46 | defp do_loop(acc \\ []) do 47 | acc = [:erlang.memory(:total) | acc] 48 | 49 | receive do 50 | :stop -> Enum.reverse(acc) 51 | after 52 | @sampling_period -> do_loop(acc) 53 | end 54 | end 55 | 56 | @impl true 57 | def stop_meassurement(task) do 58 | send(task.pid, :stop) 59 | Task.await(task) 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /benchmark/metric/message_queues_length.ex: -------------------------------------------------------------------------------- 1 | defmodule Benchmark.Metric.MessageQueuesLength do 2 | @behaviour Benchmark.Metric 3 | 4 | @tolerance_factor 0.5 5 | @sampling_period 100 6 | 7 | @impl true 8 | def assert(queues_lengths, queues_lengths_ref, test_case) do 9 | cumulative_queues_length = integrate(queues_lengths) 10 | cumulative_queues_length_ref = integrate(queues_lengths_ref) 11 | 12 | if cumulative_queues_length > 13 | cumulative_queues_length_ref * (1 + @tolerance_factor), 14 | do: 15 | raise( 16 | "The cumulative queues length has got worse! For test case: #{inspect(test_case, pretty: true)} 17 | the cumulative queues length to be: #{cumulative_queues_length_ref} and now it is: #{cumulative_queues_length}" 18 | ) 19 | 20 | :ok 21 | end 22 | 23 | @impl true 24 | def average(queues_lengths_from_multiple_tries) do 25 | queues_lengths_from_multiple_tries 26 | |> Enum.zip() 27 | |> Enum.map(&Tuple.to_list(&1)) 28 | |> Enum.map(&(Enum.sum(&1) / length(&1))) 29 | end 30 | 31 | defp integrate(queues_lengths) do 32 | Enum.sum(queues_lengths) 33 | end 34 | 35 | @impl true 36 | def start_meassurement(children_pids) do 37 | task = 38 | Task.async(fn -> 39 | do_loop([], children_pids) 40 | end) 41 | 42 | task 43 | end 44 | 45 | defp do_loop(acc, children_pids) do 46 | acc = acc ++ [meassure_queues_length(children_pids)] 47 | 48 | receive do 49 | :stop -> acc 50 | after 51 | @sampling_period -> do_loop(acc, children_pids) 52 | end 53 | end 54 | 55 | defp meassure_queues_length(children_pids) do 56 | Enum.map(children_pids, fn pid -> 57 | case Process.info(self(), :message_queue_len) do 58 | {:message_queue_len, message_queue_len} -> message_queue_len 59 | _other -> 0 60 | end 61 | end) 62 | |> Enum.sum() 63 | end 64 | 65 | @impl true 66 | def stop_meassurement(task) do 67 | send(task.pid, :stop) 68 | Task.await(task) 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /benchmark/metric/time.ex: -------------------------------------------------------------------------------- 1 | defmodule Benchmark.Metric.Time do 2 | @behaviour Benchmark.Metric 3 | 4 | @tolerance_factor 0.15 5 | 6 | @impl true 7 | def assert(time, time_ref, test_case) do 8 | if time > time_ref * (1 + @tolerance_factor), 9 | do: 10 | raise( 11 | "The time performance has got worse! For test case: #{inspect(test_case, pretty: true)} the test 12 | used to take: #{time_ref} ms and now it takes: #{time} ms" 13 | ) 14 | 15 | :ok 16 | end 17 | 18 | @impl true 19 | def average(times) do 20 | Enum.sum(times) / length(times) 21 | end 22 | 23 | @impl true 24 | def start_meassurement(_opts \\ nil) do 25 | :os.system_time(:milli_seconds) 26 | end 27 | 28 | @impl true 29 | def stop_meassurement(starting_time) do 30 | :os.system_time(:milli_seconds) - starting_time 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /benchmark/run/branched_filter.ex: -------------------------------------------------------------------------------- 1 | defmodule Benchmark.Run.BranchedFilter do 2 | @moduledoc false 3 | use Membrane.Filter 4 | 5 | alias Benchmark.Run.Reductions 6 | 7 | def_input_pad :input, accepted_format: _any, availability: :on_request 8 | def_output_pad :output, accepted_format: _any, availability: :on_request 9 | 10 | def_options number_of_reductions: [spec: integer()], 11 | generator: [spec: (integer() -> integer())], 12 | dispatcher: [spec: (integer(), integer() -> [integer()])] 13 | 14 | @impl true 15 | def handle_init(_ctx, opts) do 16 | workload_simulation = Reductions.prepare_desired_function(opts.number_of_reductions) 17 | 18 | {[], 19 | %{ 20 | buffers: [], 21 | workload_simulation: workload_simulation, 22 | generator: opts.generator, 23 | dispatcher: opts.dispatcher 24 | }} 25 | end 26 | 27 | @impl true 28 | def handle_buffer(_pad, buffer, ctx, state) do 29 | state = %{state | buffers: state.buffers ++ [buffer]} 30 | state.workload_simulation.() 31 | how_many_buffers_to_output = length(state.buffers) |> state.generator.() 32 | output_pads = Map.keys(ctx.pads) |> Enum.filter(&match?(Membrane.Pad.ref(:output, _id), &1)) 33 | 34 | how_many_buffers_per_pad = state.dispatcher.(length(output_pads), how_many_buffers_to_output) 35 | 36 | {buffers_to_output, rest_buffers} = Enum.split(state.buffers, how_many_buffers_to_output) 37 | actions = prepare_actions(buffers_to_output, output_pads, how_many_buffers_per_pad) 38 | state = %{state | buffers: rest_buffers} 39 | {actions, state} 40 | end 41 | 42 | defp prepare_actions(buffers, output_pads, how_many_buffers_per_pad) do 43 | {actions, rest_of_buffers} = 44 | Enum.zip(output_pads, how_many_buffers_per_pad) 45 | |> Enum.map_reduce(buffers, fn {pad, how_many_buffers_per_pad}, buffers_left -> 46 | {buffers_for_this_pad, rest_buffers} = Enum.split(buffers_left, how_many_buffers_per_pad) 47 | action = {:buffer, {pad, buffers_for_this_pad}} 48 | {action, rest_buffers} 49 | end) 50 | 51 | if rest_of_buffers != [] do 52 | raise("The dispatcher function is working improperly!") 53 | end 54 | 55 | actions 56 | end 57 | 58 | @impl true 59 | def handle_end_of_stream(_pad, ctx, state) do 60 | all_input_pads_eos? = 61 | Enum.filter(ctx.pads, fn {_pad_ref, pad} -> pad.direction == :input end) 62 | |> Enum.all?(fn {_input_pad_ref, input_pad} -> input_pad.end_of_stream? == true end) 63 | 64 | if all_input_pads_eos? do 65 | output_pads_without_eos = 66 | Enum.filter(ctx.pads, fn {_pad_ref, pad} -> 67 | pad.end_of_stream? == false and pad.direction == :output 68 | end) 69 | |> Enum.map(fn {pad_ref, _pad} -> pad_ref end) 70 | 71 | actions = Enum.map(output_pads_without_eos, &{:end_of_stream, &1}) 72 | {actions, state} 73 | else 74 | {[], state} 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /benchmark/run/linear_filter.ex: -------------------------------------------------------------------------------- 1 | defmodule Benchmark.Run.LinearFilter do 2 | @moduledoc false 3 | use Membrane.Filter 4 | 5 | alias Benchmark.Run.Reductions 6 | 7 | def_input_pad :input, accepted_format: _any 8 | def_output_pad :output, accepted_format: _any 9 | 10 | def_options number_of_reductions: [spec: integer()], 11 | generator: [spec: (integer() -> integer())] 12 | 13 | @impl true 14 | def handle_init(_ctx, opts) do 15 | workload_simulation = Reductions.prepare_desired_function(opts.number_of_reductions) 16 | state = %{buffers: [], workload_simulation: workload_simulation, generator: opts.generator} 17 | {[], state} 18 | end 19 | 20 | @impl true 21 | def handle_buffer(_pad, buffer, _ctx, state) do 22 | state.workload_simulation.() 23 | state = %{state | buffers: state.buffers ++ [buffer]} 24 | how_many_buffers_to_output = length(state.buffers) |> state.generator.() 25 | 26 | {buffers_to_output, rest_buffers} = Enum.split(state.buffers, how_many_buffers_to_output) 27 | state = %{state | buffers: rest_buffers} 28 | {[buffer: {:output, buffers_to_output}], state} 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /benchmark/run/pipeline.ex: -------------------------------------------------------------------------------- 1 | defmodule Benchmark.Run.Pipeline do 2 | @moduledoc false 3 | use Membrane.Pipeline 4 | 5 | @impl true 6 | def handle_init(_ctx, options) do 7 | {[spec: options[:spec]], %{monitoring_process: options[:monitoring_process]}} 8 | end 9 | 10 | @impl true 11 | def handle_call(:get_children_pids, ctx, state) do 12 | children_pids = 13 | Enum.map(ctx.children, fn {_child_name, child_entry} -> 14 | child_entry.pid 15 | end) 16 | 17 | {[reply: children_pids], state} 18 | end 19 | 20 | @impl true 21 | def handle_element_end_of_stream(:sink, _pad, _ctx, state) do 22 | send(state.monitoring_process, :sink_eos) 23 | {[], state} 24 | end 25 | 26 | def handle_element_end_of_stream(_not_sink, _pad, _ctx, state) do 27 | {[], state} 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /benchmark/run/reductions.ex: -------------------------------------------------------------------------------- 1 | defmodule Benchmark.Run.Reductions do 2 | @moduledoc false 3 | 4 | @n1 100 5 | @n2 1_000 6 | 7 | defp test_function, do: :rand.uniform() 8 | 9 | defp meassure(n) do 10 | task = 11 | Task.async(fn -> 12 | Enum.each(1..n, fn _x -> test_function() end) 13 | Process.info(self(), :reductions) |> elem(1) 14 | end) 15 | 16 | Task.await(task) 17 | end 18 | 19 | defp calculate do 20 | r1 = meassure(@n1) 21 | r2 = meassure(@n2) 22 | 23 | {r1, r2} 24 | end 25 | 26 | @spec prepare_desired_function(non_neg_integer()) :: (-> any()) 27 | def prepare_desired_function(how_many_reductions) do 28 | {r1, r2} = calculate() 29 | n = trunc((how_many_reductions - r2) / (r2 - r1) * (@n2 - @n1) + @n2) 30 | fn -> Enum.each(1..n, fn _x -> test_function() end) end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | if config_env() == :test do 4 | config :junit_formatter, include_filename?: true 5 | 6 | config :membrane_core, :telemetry_flags, 7 | tracked_callbacks: [ 8 | element: [ 9 | :handle_init, 10 | :handle_playing, 11 | :handle_setup, 12 | :handle_terminate_request, 13 | :handle_parent_notification 14 | ], 15 | bin: :all, 16 | pipeline: :all 17 | ], 18 | datapoints: :all 19 | end 20 | -------------------------------------------------------------------------------- /example.livemd: -------------------------------------------------------------------------------- 1 | # Membrane Quick Start 2 | 3 | ```elixir 4 | Logger.configure(level: :info) 5 | 6 | Mix.install([ 7 | :membrane_hackney_plugin, 8 | :membrane_mp3_mad_plugin, 9 | :membrane_portaudio_plugin 10 | ]) 11 | ``` 12 | 13 | ## Play mp3 14 | 15 | ```elixir 16 | defmodule MyPipeline do 17 | use Membrane.Pipeline 18 | 19 | @impl true 20 | def handle_init(_ctx, mp3_url) do 21 | spec = 22 | child(%Membrane.Hackney.Source{ 23 | location: mp3_url, 24 | hackney_opts: [follow_redirect: true] 25 | }) 26 | |> child(Membrane.MP3.MAD.Decoder) 27 | |> child(Membrane.PortAudio.Sink) 28 | 29 | {[spec: spec], %{}} 30 | end 31 | end 32 | 33 | mp3_url = 34 | "https://raw.githubusercontent.com/membraneframework/membrane_demo/master/simple_pipeline/sample.mp3" 35 | 36 | {:ok, _supervisor, pipeline} = Membrane.Pipeline.start_link(MyPipeline, mp3_url) 37 | ``` 38 | 39 | ## Terminate the pipeline 40 | 41 | ```elixir 42 | Membrane.Pipeline.terminate(pipeline) 43 | ``` 44 | -------------------------------------------------------------------------------- /guides/upgrading/v0.12.md: -------------------------------------------------------------------------------- 1 | # Upgrading to v0.12 2 | 3 | Between v0.11 and v0.12 some breaking changes have occurred, so here comes the guide that will help you adjust your code to the new API. See the [release notes](https://github.com/membraneframework/membrane_core/releases/tag/v0.12.1) for details. 4 | 5 | ## Deps upgrade 6 | 7 | Upgrade `membrane_core` to `v0.12.1`. 8 | 9 | ```elixir 10 | defp deps do 11 | [ 12 | {:membrane_core, "~> 0.12.1"}, 13 | ... 14 | ] 15 | end 16 | ``` 17 | 18 | ## Implement `handle_child_pad_removed/4` callback in bins and pipelines, if it is needed 19 | 20 | Now, if bin removes its pad (e.g. by removing an element linked to the bin's inner pad), bin's parent has to have implemented proper `handle_child_pad_removed/4` callback, to handle it. If there is no such a callback, default behaviour is to raise an error. 21 | 22 | ```elixir 23 | @impl true 24 | def handle_child_pad_removed(:rtp, Pad.ref(:rtp_input, _ssrc), _ctx, state) do 25 | # ... 26 | end 27 | ``` 28 | 29 | ## Remove `:playback` action 30 | 31 | Now, membrane pipelines enter the playing playback by default and they don't have to return a `:playback` action to do it. 32 | 33 | ```diff 34 | - @impl true 35 | - def handle_setup(_ctx, state) do 36 | - {[playback: :playing], state} 37 | - end 38 | ``` 39 | Instead of it, there is a new action introduced in `membrane_core` v0.12, `setup: :incomplete | :complete`. If you want to defer a moment when a component enters the playing playback, you can return `{:setup, :incomplete}` action from `handle_setup` callback. If you do that, a component will enter the playing playback only when you return `{:setup, :complete}` action from another callback, e.g. `handle_info`. 40 | 41 | ```diff 42 | - @impl true 43 | - def handle_setup(_ctx, state) do 44 | - Process.send_after(self(), :play, 1000) 45 | - {[], state} 46 | - end 47 | - 48 | - @impl true 49 | - def handle_info(:play, _ctx, state) do 50 | - {[playback: :playing], state} 51 | - end 52 | 53 | + @impl true 54 | + def handle_setup(_ctx, state) do 55 | + Process.send_after(self(), :play, 1000) 56 | + {[setup: :incomplete], state} 57 | + end 58 | + 59 | + @impl true 60 | + def handle_info(:play, _ctx, state) do 61 | + {[setup: :complete], state} 62 | + end 63 | ``` 64 | 65 | `:setup` action is available not only in pipelines but in bins and elements as well. 66 | -------------------------------------------------------------------------------- /guides/upgrading/v1.0.0-rc1.md: -------------------------------------------------------------------------------- 1 | # Upgrading to v1.0.0-rc1 2 | 3 | ### Remove `handle_buffers_batch/4`/`handle_process_list/4`/`handle_write_list/4` callback 4 | 5 | In `v1.0.0-rc1` we removed `handle_buffers_batch/4` callback. Instead of handling list of buffer in this callback, you have to handle every single buffer separately in `handle_buffer/4` callback. 6 | 7 | ```diff 8 | @impl true 9 | - def handle_buffers_batch(pad_ref, buffers_list, ctx, state) do 10 | + def handle_buffer(pad_ref, buffer, ctx, state) do 11 | ... 12 | end 13 | ``` 14 | 15 | Notice, if you are upgrading your code from `v0.12.*` directly to `v1.0.0-rc1`, omitting `v1.0.0-rc0`, there is a chance, that your elements still have callback named `handle_process_list/4` or `handle_write_list/4`, instead of `handle_buffers_batch/4`. 16 | 17 | ```diff 18 | @impl true 19 | - def handle_process_list(pad_ref, buffers_list, ctx, state) do 20 | + def handle_buffer(pad_ref, buffer, ctx, state) do 21 | ... 22 | end 23 | ``` 24 | 25 | ```diff 26 | @impl true 27 | - def handle_write_list(pad_ref, buffers_list, ctx, state) do 28 | + def handle_buffer(pad_ref, buffer, ctx, state) do 29 | ... 30 | end 31 | ``` 32 | 33 | ### Implement you own `start/*`, `start_link/*` or `terminate/1` function (if you want to) 34 | 35 | Until `v1.0.0-rc0`, Membrane has generated `start/2`, `start_link/2`, and `terminate/1` functions in modules using `Membrane.Pipeline`, if code developer hadn't done it explicitly. Since `v1.0.0-rc1`, if you want to have these functions implemented in your pipeline module, you have to implement them on your own. Alternatively, you can always call `Membrane.Pipeline.start(YourPipelineModule, init_arg, opts)`, `Membrane.Pipeline.start_link(YourPipelineModule, init_arg, opts)`, and `Membrane.Pipeline.terminate(pipeline_pid)` from beyond `YourPipelineModule` without wrapping it into public functions. 36 | 37 | ### Rename `:structure` option passed to the `Membrane.Testing.Pipeline` into `:spec` 38 | 39 | ```diff 40 | import Membrane.ChildrenSpec 41 | 42 | spec = 43 | child(:source, %Membrane.Testing.Source{some_field: some_arg}) 44 | |> child(:sink), %Membrane.Testing.Sink{another_filed: another_arg} 45 | 46 | - pipeline = Membrane.Testing.Pipeline.start_link_supervised(structure: spec) 47 | + pipeline = Membrane.Testing.Pipeline.start_link_supervised(spec: spec) 48 | ``` 49 | -------------------------------------------------------------------------------- /lib/membrane/bin/callback_context.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Bin.CallbackContext do 2 | @moduledoc """ 3 | Module describing context passed to the `Membrane.Bin` callbacks. 4 | """ 5 | 6 | @typedoc """ 7 | Type describing context passed to the `Membrane.Bin` callbacks. 8 | 9 | Field `:pad_options` is present only in `c:Membrane.Bin.handle_pad_added/3` 10 | and `c:Membrane.Bin.handle_pad_removed/3`. 11 | 12 | Field `:start_of_stream_received?` is present only in 13 | `c:Membrane.Bin.handle_element_end_of_stream/4`. 14 | 15 | Field `:crash_initiator` is only present in `c:Membrane.Bin.handle_child_terminated/3` 16 | and `c:Membrane.Bin.handle_crash_group_down/3`. 17 | 18 | Fields `:members` and `:crash_reason` are present only in 19 | `c:Membrane.Bin.handle_crash_group_down/3`. 20 | 21 | Fields `:exit_reason` and `:group_name` are present only in 22 | `c:Membrane.Bin.handle_child_terminated/3`. 23 | """ 24 | @type t :: %{ 25 | :children => %{Membrane.Child.name() => Membrane.ChildEntry.t()}, 26 | :clock => Membrane.Clock.t(), 27 | :parent_clock => Membrane.Clock.t(), 28 | :module => module(), 29 | :name => Membrane.Bin.name(), 30 | :pads => %{Membrane.Pad.ref() => Membrane.Bin.PadData.t()}, 31 | :playback => Membrane.Playback.t(), 32 | :resource_guard => Membrane.ResourceGuard.t(), 33 | :utility_supervisor => Membrane.UtilitySupervisor.t(), 34 | optional(:pad_options) => map(), 35 | optional(:members) => [Membrane.Child.name()], 36 | optional(:crash_initiator) => Membrane.Child.name() | nil, 37 | optional(:crash_reason) => :normal | :shutdown | {:shutdown, term()} | term(), 38 | optional(:start_of_stream_received?) => boolean(), 39 | optional(:exit_reason) => :normal | :shutdown | {:shutdown, term()} | term(), 40 | optional(:group_name) => Membrane.Child.group() | nil 41 | } 42 | end 43 | -------------------------------------------------------------------------------- /lib/membrane/bin/pad_data.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Bin.PadData do 2 | @moduledoc """ 3 | Struct describing current pad state. 4 | 5 | The public fields are: 6 | - `:availability` - see `t:Membrane.Pad.availability/0` 7 | - `:direction` - see `t:Membrane.Pad.direction/0` 8 | - `:name` - see `t:Membrane.Pad.name/0`. Do not mistake with `:ref` 9 | - `:options` - options passed in `Membrane.ChildrenSpec` when linking pad 10 | - `:ref` - see `t:Membrane.Pad.ref/0` 11 | - `max_instances` - specifies maximal possible number of instances of a dynamic pad that can exist within single element. Equals `nil` for pads with `availability: :always`. 12 | 13 | Other fields in the struct ARE NOT PART OF THE PUBLIC API and should not be 14 | accessed or relied on. 15 | """ 16 | use Bunch.Access 17 | 18 | @type private_field :: term() 19 | 20 | @typedoc @moduledoc 21 | @type t :: %__MODULE__{ 22 | ref: Membrane.Pad.ref(), 23 | options: Membrane.ChildrenSpec.pad_options(), 24 | availability: Membrane.Pad.availability(), 25 | direction: Membrane.Pad.direction(), 26 | name: Membrane.Pad.name(), 27 | max_instances: Membrane.Pad.max_instances() | nil, 28 | spec_ref: private_field, 29 | link_id: private_field, 30 | endpoint: private_field, 31 | linked?: private_field, 32 | response_received?: private_field, 33 | linking_timeout_id: private_field, 34 | linked_in_spec?: private_field 35 | } 36 | 37 | @enforce_keys [ 38 | :ref, 39 | :options, 40 | :availability, 41 | :direction, 42 | :name, 43 | :link_id, 44 | :endpoint, 45 | :linked?, 46 | :response_received?, 47 | :spec_ref, 48 | :linking_timeout_id, 49 | :linked_in_spec? 50 | ] 51 | 52 | defstruct @enforce_keys ++ [:max_instances] 53 | end 54 | -------------------------------------------------------------------------------- /lib/membrane/buffer.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Buffer do 2 | @moduledoc """ 3 | Structure representing a single chunk of data that flows between elements. 4 | 5 | For now, it is just a wrapper around bitstring with optionally some metadata 6 | attached to it, but in future releases we plan to support different payload 7 | types. 8 | """ 9 | 10 | alias __MODULE__ 11 | alias Membrane.Payload 12 | alias Membrane.Time 13 | 14 | @type metadata :: map 15 | 16 | @typedoc @moduledoc 17 | @type t :: %Buffer{ 18 | pts: Time.t() | nil, 19 | dts: Time.t() | nil, 20 | payload: Payload.t(), 21 | metadata: metadata 22 | } 23 | 24 | @enforce_keys [:payload] 25 | defstruct @enforce_keys ++ [pts: nil, dts: nil, metadata: Map.new()] 26 | 27 | @doc """ 28 | Returns `t:Membrane.Buffer.t/0` `:dts` if available or `:pts` if `:dts` is not set. 29 | If none of them is set `nil` is returned. 30 | """ 31 | @spec get_dts_or_pts(__MODULE__.t()) :: Time.t() | nil 32 | def get_dts_or_pts(buffer) do 33 | buffer.dts || buffer.pts 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/membrane/buffer/metric.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Buffer.Metric do 2 | @moduledoc """ 3 | Specifies API for metrics that analyze data in terms of a given unit 4 | """ 5 | 6 | alias Membrane.Buffer 7 | alias __MODULE__ 8 | 9 | @type unit :: :buffers | :bytes 10 | 11 | @callback buffer_size_approximation() :: pos_integer 12 | 13 | @callback buffers_size([%Buffer{}] | []) :: non_neg_integer 14 | 15 | @callback split_buffers([%Buffer{}] | [], non_neg_integer) :: 16 | {[%Buffer{}] | [], [%Buffer{}] | []} 17 | 18 | @spec from_unit(unit()) :: module() 19 | def from_unit(:buffers), do: Metric.Count 20 | def from_unit(:bytes), do: Metric.ByteSize 21 | end 22 | -------------------------------------------------------------------------------- /lib/membrane/buffer/metric/byte_size.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Buffer.Metric.ByteSize do 2 | @moduledoc """ 3 | Implementation of `Membrane.Buffer.Metric` for the `:bytes` unit 4 | """ 5 | 6 | @behaviour Membrane.Buffer.Metric 7 | 8 | alias Membrane.{Buffer, Payload} 9 | 10 | @impl true 11 | def buffer_size_approximation, do: 1500 12 | 13 | @impl true 14 | def buffers_size(buffers), 15 | do: buffers |> Enum.reduce(0, fn %Buffer{payload: p}, acc -> acc + Payload.size(p) end) 16 | 17 | @impl true 18 | def split_buffers(buffers, count), do: do_split_buffers(buffers, count, []) 19 | 20 | defp do_split_buffers(buffers, at_pos, acc) when at_pos == 0 or buffers == [] do 21 | {acc |> Enum.reverse(), buffers} 22 | end 23 | 24 | defp do_split_buffers([%Buffer{payload: p} = buf | rest], at_pos, acc) when at_pos > 0 do 25 | if at_pos < Payload.size(p) do 26 | {p1, p2} = Payload.split_at(p, at_pos) 27 | acc = [%Buffer{buf | payload: p1} | acc] |> Enum.reverse() 28 | rest = [%Buffer{buf | payload: p2} | rest] 29 | {acc, rest} 30 | else 31 | do_split_buffers(rest, at_pos - Payload.size(p), [buf | acc]) 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/membrane/buffer/metric/count.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Buffer.Metric.Count do 2 | @moduledoc """ 3 | Implementation of `Membrane.Buffer.Metric` for the `:buffers` unit 4 | """ 5 | 6 | @behaviour Membrane.Buffer.Metric 7 | 8 | @impl true 9 | def buffer_size_approximation, do: 1 10 | 11 | @impl true 12 | def buffers_size(buffers), do: length(buffers) 13 | 14 | @impl true 15 | def split_buffers(buffers, count), do: buffers |> Enum.split(count) 16 | end 17 | -------------------------------------------------------------------------------- /lib/membrane/child.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Child do 2 | @moduledoc """ 3 | Module that keeps track of types used by both elements and bins 4 | """ 5 | import Membrane.Element, only: [is_element_name?: 1] 6 | import Membrane.Bin, only: [is_bin_name?: 1] 7 | 8 | alias Membrane.{Bin, Element} 9 | 10 | @typedoc "Any type except for `nil`" 11 | @type non_nil :: any() 12 | 13 | @typedoc "Name of the child" 14 | @type name :: Element.name() | Bin.name() 15 | 16 | @typedoc """ 17 | Specifies the children group name. 18 | 19 | Can be any type except for `nil` 20 | """ 21 | @type group() :: non_nil() 22 | 23 | @typedoc "Options of the child" 24 | @type options :: Element.options() | Bin.options() 25 | 26 | defguard is_child_name?(arg) when is_element_name?(arg) or is_bin_name?(arg) 27 | end 28 | -------------------------------------------------------------------------------- /lib/membrane/child_entry.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.ChildEntry do 2 | @moduledoc """ 3 | Struct describing child entry of a parent. 4 | 5 | The public fields are: 6 | - `name` - child name 7 | - `module` - child module 8 | - `group` - child group name 9 | - `options` - options passed to the child 10 | - `component_type` - either `:element` or `:bin` 11 | - `playback` - either `:stopped` or `:playing` 12 | 13 | Other fields in the struct ARE NOT PART OF THE PUBLIC API and should not be 14 | accessed or relied on. 15 | """ 16 | use Bunch.Access 17 | 18 | @typedoc @moduledoc 19 | @type t :: %__MODULE__{ 20 | name: Membrane.Child.name(), 21 | module: module, 22 | options: struct | nil, 23 | component_type: :element | :bin, 24 | pid: pid, 25 | clock: Membrane.Clock.t(), 26 | sync: Membrane.Sync.t(), 27 | terminating?: boolean(), 28 | group: Membrane.Child.group() 29 | } 30 | 31 | defstruct [ 32 | :name, 33 | :module, 34 | :options, 35 | :component_type, 36 | :pid, 37 | :clock, 38 | :sync, 39 | :spec_ref, 40 | :group, 41 | initialized?: false, 42 | ready?: false, 43 | terminating?: false 44 | ] 45 | end 46 | -------------------------------------------------------------------------------- /lib/membrane/component_path.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.ComponentPath do 2 | @moduledoc """ 3 | A list consisting of following pipeline/bin/element names down the assembled pipeline. 4 | 5 | It traces element's path inside a pipeline. 6 | Information is being stored in a process dictionary and can be set/appended to. 7 | """ 8 | 9 | @typedoc @moduledoc 10 | @type path :: list(String.t()) 11 | @type formatted_path :: String.t() 12 | 13 | @key :membrane_path 14 | 15 | @doc """ 16 | Sets current path. 17 | 18 | If path had already existed then replaces it. 19 | """ 20 | @spec set(path()) :: :ok 21 | def set(path) do 22 | Process.put(@key, path) 23 | :ok 24 | end 25 | 26 | @doc """ 27 | Returns formatted string of given path's names. 28 | """ 29 | @spec format(path()) :: String.t() 30 | def format(path) do 31 | Enum.join(path) 32 | end 33 | 34 | @doc """ 35 | Works the same way as `format/1` but uses currently stored path. 36 | """ 37 | @spec get_formatted() :: String.t() 38 | def get_formatted() do 39 | get() |> format() 40 | end 41 | 42 | @doc """ 43 | Returns currently stored path. 44 | 45 | If path has not been set, empty list is returned. 46 | """ 47 | @spec get() :: path() 48 | def get(), do: Process.get(@key, []) 49 | end 50 | -------------------------------------------------------------------------------- /lib/membrane/core/bin/callback_context.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Core.Bin.CallbackContext do 2 | @moduledoc false 3 | 4 | @type optional_fields :: 5 | [pad_options: map()] 6 | | [ 7 | members: [Membrane.Child.name()], 8 | crash_initiator: Membrane.Child.name(), 9 | crash_reason: :normal | :shutdown | {:shutdown, term()} | term() 10 | ] 11 | | [start_of_stream_received?: boolean()] 12 | | [ 13 | group_name: Membrane.Child.group() | nil, 14 | crash_initiator: Membrane.Child.name() | nil, 15 | exit_reason: :normal | :shutdown | {:shutdown, term()} | term() 16 | ] 17 | 18 | @spec from_state(Membrane.Core.Bin.State.t(), optional_fields()) :: 19 | Membrane.Bin.CallbackContext.t() 20 | def from_state(state, optional_fields \\ []) do 21 | Map.new(optional_fields) 22 | |> Map.merge(%{ 23 | children: state.children, 24 | clock: state.synchronization.clock, 25 | parent_clock: state.synchronization.parent_clock, 26 | pads: state.pads_data, 27 | module: state.module, 28 | name: state.name, 29 | playback: state.playback, 30 | resource_guard: state.resource_guard, 31 | utility_supervisor: state.subprocess_supervisor 32 | }) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/membrane/core/bin/zombie.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Core.Bin.Zombie do 2 | @moduledoc false 3 | # When a bin returns Membrane.Bin.Action.terminate() and becomes a zombie 4 | # this module is used to replace the user implementation of the bin 5 | use Membrane.Bin 6 | require Membrane.Logger 7 | 8 | # Overrides all the overridable callbacks to add a debug message that the original 9 | # implementation is not called 10 | Membrane.Bin.behaviour_info(:callbacks) 11 | |> Enum.filter(&Module.overridable?(__MODULE__, &1)) 12 | |> Enum.map(fn {name, arity} -> 13 | args = Enum.map(1..arity//1, &Macro.var(:"arg#{&1}", __MODULE__)) 14 | 15 | @impl true 16 | def unquote(name)(unquote_splicing(args)) do 17 | Membrane.Logger.debug( 18 | "Not calling the #{unquote(name)} callback with the following arguments: 19 | #{Enum.map_join(unquote(args), ", ", &inspect/1)} 20 | because the bin is in the zombie mode" 21 | ) 22 | 23 | super(unquote_splicing(args)) 24 | end 25 | end) 26 | end 27 | -------------------------------------------------------------------------------- /lib/membrane/core/child.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Core.Child do 2 | @moduledoc false 3 | 4 | @type state :: Membrane.Core.Element.State.t() | Membrane.Core.Bin.State.t() 5 | 6 | @docs_order [ 7 | :membrane_options_moduledoc, 8 | :membrane_pads_moduledoc, 9 | :membrane_clock_moduledoc 10 | ] 11 | 12 | @spec generate_moduledoc(module, :element | :bin) :: Macro.t() 13 | def generate_moduledoc(module, child_type) do 14 | membrane_pads_moduledoc = 15 | Module.get_attribute(module, :membrane_pads) 16 | |> __MODULE__.PadsSpecs.generate_docs_from_pads_specs() 17 | 18 | Module.put_attribute(module, :membrane_pads_moduledoc, membrane_pads_moduledoc) 19 | 20 | moduledoc = 21 | case Module.get_attribute(module, :moduledoc, "") do 22 | {_line, doc} -> doc 23 | doc -> doc 24 | end 25 | 26 | if is_binary(moduledoc) and String.valid?(moduledoc) do 27 | moduledoc = 28 | if String.trim(moduledoc) == "" do 29 | "Membrane #{child_type}.\n\n" 30 | else 31 | moduledoc 32 | end 33 | 34 | moduledoc = 35 | @docs_order 36 | |> Enum.map(&Module.get_attribute(module, &1)) 37 | |> Enum.filter(& &1) 38 | |> Enum.reduce(moduledoc, fn doc, moduledoc -> 39 | quote do 40 | unquote(moduledoc) <> unquote(doc) 41 | end 42 | end) 43 | 44 | quote do 45 | @moduledoc unquote(moduledoc) 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/membrane/core/child/lifecycle_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Core.Child.LifecycleController do 2 | @moduledoc false 3 | alias Membrane.Core.{CallbackHandler, Component} 4 | 5 | require Membrane.Core.Component 6 | 7 | @spec handle_parent_notification(Membrane.ParentNotification.t(), Membrane.Core.Child.state()) :: 8 | Membrane.Core.Child.state() 9 | def handle_parent_notification(notification, state) do 10 | action_handler = Component.action_handler(state) 11 | 12 | CallbackHandler.exec_and_handle_callback( 13 | :handle_parent_notification, 14 | action_handler, 15 | %{context: &Component.context_from_state/1}, 16 | notification, 17 | state 18 | ) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/membrane/core/child/pad_spec_handler.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Core.Child.PadSpecHandler do 2 | @moduledoc false 3 | 4 | # Module parsing pads specifications in elements and bins. 5 | 6 | use Bunch 7 | 8 | alias Membrane.Core.{Bin, Child, Element} 9 | alias Membrane.Pad 10 | 11 | require Membrane.Pad 12 | 13 | @doc """ 14 | Initializes pads info basing on element's or bin's pads specifications. 15 | """ 16 | @spec init_pads(Element.State.t()) :: Element.State.t() 17 | @spec init_pads(Bin.State.t()) :: Bin.State.t() 18 | def init_pads(state) do 19 | %{ 20 | state 21 | | pads_info: 22 | get_pads(state) 23 | |> Map.new(), 24 | pads_data: %{} 25 | } 26 | end 27 | 28 | @spec get_pads(Child.state()) :: [{Pad.name(), Pad.description()}] 29 | def get_pads(%Bin.State{module: module}) do 30 | module.membrane_pads() 31 | end 32 | 33 | def get_pads(%Element.State{module: module}) do 34 | module.membrane_pads() 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/membrane/core/component.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Core.Component do 2 | @moduledoc false 3 | 4 | @type state :: 5 | Membrane.Core.Pipeline.State.t() 6 | | Membrane.Core.Bin.State.t() 7 | | Membrane.Core.Element.State.t() 8 | 9 | @type callback_context_optional_fields :: 10 | Membrane.Core.Element.CallbackContext.optional_fields() 11 | | Membrane.Core.Bin.CallbackContext.optional_fields() 12 | | Membrane.Core.Pipeline.CallbackContext.optional_fields() 13 | 14 | @type callback_context :: 15 | Membrane.Element.CallbackContext.t() 16 | | Membrane.Bin.CallbackContext.t() 17 | | Membrane.Pipeline.CallbackContext.t() 18 | 19 | @spec action_handler(state) :: module 20 | [Pipeline, Bin, Element] 21 | |> Enum.map(fn component -> 22 | def action_handler(%unquote(Module.concat([Membrane.Core, component, State])){}), 23 | do: unquote(Module.concat([Membrane.Core, component, ActionHandler])) 24 | end) 25 | 26 | @spec context_from_state(state(), callback_context_optional_fields()) :: 27 | callback_context() 28 | def context_from_state(state, args \\ []) do 29 | alias Membrane.Core.{Bin, Element, Pipeline} 30 | 31 | callback_context_module = 32 | case state do 33 | %Element.State{} -> Element.CallbackContext 34 | %Bin.State{} -> Bin.CallbackContext 35 | %Pipeline.State{} -> Pipeline.CallbackContext 36 | end 37 | 38 | callback_context_module.from_state(state, args) 39 | end 40 | 41 | @spec pipeline?(state) :: boolean() 42 | def pipeline?(%Membrane.Core.Pipeline.State{}), do: true 43 | def pipeline?(_state), do: false 44 | 45 | @spec element?(state) :: boolean() 46 | def element?(%Membrane.Core.Element.State{}), do: true 47 | def element?(_state), do: false 48 | 49 | @spec bin?(state) :: boolean() 50 | def bin?(%Membrane.Core.Bin.State{}), do: true 51 | def bin?(_state), do: false 52 | 53 | @spec child?(state) :: boolean() 54 | def child?(state), do: element?(state) or bin?(state) 55 | 56 | @spec parent?(state) :: boolean() 57 | def parent?(state), do: pipeline?(state) or bin?(state) 58 | end 59 | -------------------------------------------------------------------------------- /lib/membrane/core/element/atomic_demand/atomic_flow_status.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Core.Element.AtomicDemand.AtomicFlowStatus do 2 | @moduledoc false 3 | 4 | alias Membrane.Core.Element.AtomicDemand.DistributedAtomic 5 | alias Membrane.Core.Element.EffectiveFlowController 6 | 7 | @type t :: DistributedAtomic.t() 8 | @type value :: {:resolved, EffectiveFlowController.effective_flow_control()} | :to_be_resolved 9 | 10 | @spec new(value, supervisor: pid()) :: t 11 | def new(initial_value, supervisor: supervisor) do 12 | initial_value 13 | |> flow_status_to_int() 14 | |> DistributedAtomic.new(supervisor: supervisor) 15 | end 16 | 17 | @spec get(t) :: value() 18 | def get(distributed_atomic) do 19 | distributed_atomic 20 | |> DistributedAtomic.get() 21 | |> int_to_flow_status() 22 | end 23 | 24 | @spec set(t, value()) :: :ok 25 | def set(distributed_atomic, value) do 26 | value = flow_status_to_int(value) 27 | DistributedAtomic.set(distributed_atomic, value) 28 | end 29 | 30 | defp int_to_flow_status(0), do: :to_be_resolved 31 | defp int_to_flow_status(1), do: {:resolved, :push} 32 | defp int_to_flow_status(2), do: {:resolved, :pull} 33 | 34 | defp flow_status_to_int(:to_be_resolved), do: 0 35 | defp flow_status_to_int({:resolved, :push}), do: 1 36 | defp flow_status_to_int({:resolved, :pull}), do: 2 37 | end 38 | -------------------------------------------------------------------------------- /lib/membrane/core/element/atomic_demand/distributed_atomic.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Core.Element.AtomicDemand.DistributedAtomic do 2 | @moduledoc false 3 | 4 | # A module providing a common interface to access and modify a counter used in the AtomicDemand implementation. 5 | # The counter uses :atomics module under the hood. 6 | # The module allows to create and modify the value of a counter in the same manner both when the counter is about to be accessed 7 | # from the same node, and from different nodes. 8 | 9 | alias __MODULE__.Worker 10 | alias Membrane.Core.SubprocessSupervisor 11 | 12 | @enforce_keys [:worker, :atomic_ref] 13 | defstruct @enforce_keys 14 | 15 | @type t :: %__MODULE__{worker: Worker.t(), atomic_ref: :atomics.atomics_ref()} 16 | 17 | defguardp on_the_same_node_as_self(distributed_atomic) 18 | when distributed_atomic.worker |> node() == self() |> node() 19 | 20 | @spec new(integer() | nil, supervisor: pid()) :: t 21 | def new(initial_value \\ nil, supervisor: supervisor) do 22 | atomic_ref = :atomics.new(1, []) 23 | {:ok, worker} = SubprocessSupervisor.start_utility(supervisor, Worker) 24 | 25 | distributed_atomic = %__MODULE__{ 26 | atomic_ref: atomic_ref, 27 | worker: worker 28 | } 29 | 30 | if initial_value != nil do 31 | :ok = set(distributed_atomic, initial_value) 32 | end 33 | 34 | distributed_atomic 35 | end 36 | 37 | @spec add_get(t, integer()) :: integer() 38 | def add_get(%__MODULE__{} = distributed_atomic, value) 39 | when on_the_same_node_as_self(distributed_atomic) do 40 | :atomics.add_get(distributed_atomic.atomic_ref, 1, value) 41 | end 42 | 43 | def add_get(%__MODULE__{} = distributed_atomic, value) do 44 | GenServer.call(distributed_atomic.worker, {:add_get, distributed_atomic.atomic_ref, value}) 45 | end 46 | 47 | @spec sub_get(t, integer()) :: integer() 48 | def sub_get(%__MODULE__{} = distributed_atomic, value) 49 | when on_the_same_node_as_self(distributed_atomic) do 50 | :atomics.sub_get(distributed_atomic.atomic_ref, 1, value) 51 | end 52 | 53 | def sub_get(%__MODULE__{} = distributed_atomic, value) do 54 | GenServer.call(distributed_atomic.worker, {:sub_get, distributed_atomic.atomic_ref, value}) 55 | end 56 | 57 | @spec set(t, integer()) :: :ok 58 | def set(%__MODULE__{} = distributed_atomic, value) 59 | when on_the_same_node_as_self(distributed_atomic) do 60 | :atomics.put(distributed_atomic.atomic_ref, 1, value) 61 | end 62 | 63 | def set(%__MODULE__{} = distributed_atomic, value) do 64 | GenServer.cast(distributed_atomic.worker, {:put, distributed_atomic.atomic_ref, value}) 65 | end 66 | 67 | @spec get(t) :: integer() 68 | def get(%__MODULE__{} = distributed_atomic) 69 | when on_the_same_node_as_self(distributed_atomic) do 70 | :atomics.get(distributed_atomic.atomic_ref, 1) 71 | end 72 | 73 | def get(%__MODULE__{} = distributed_atomic) do 74 | GenServer.call(distributed_atomic.worker, {:get, distributed_atomic.atomic_ref}) 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/membrane/core/element/atomic_demand/distributed_atomic/worker.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Core.Element.AtomicDemand.DistributedAtomic.Worker do 2 | @moduledoc false 3 | 4 | # This is a GenServer created when the counter is about to be accessed from different nodes - it's running on the same node, 5 | # where the :atomics variable is put, and processes from different nodes can ask it to modify the counter on their behalf. 6 | 7 | use GenServer 8 | 9 | require Membrane.Core.Utils, as: Utils 10 | 11 | @type t :: pid() 12 | 13 | @spec start_link(any()) :: {:ok, t} 14 | def start_link(opts), do: GenServer.start_link(__MODULE__, opts) 15 | 16 | @impl true 17 | def init(_opts) do 18 | {:ok, nil, :hibernate} 19 | end 20 | 21 | @impl true 22 | def handle_call({:add_get, atomic_ref, value}, _from, _state) do 23 | Utils.log_on_error do 24 | result = :atomics.add_get(atomic_ref, 1, value) 25 | {:reply, result, nil} 26 | end 27 | end 28 | 29 | @impl true 30 | def handle_call({:sub_get, atomic_ref, value}, _from, _state) do 31 | Utils.log_on_error do 32 | result = :atomics.sub_get(atomic_ref, 1, value) 33 | {:reply, result, nil} 34 | end 35 | end 36 | 37 | @impl true 38 | def handle_call({:get, atomic_ref}, _from, _state) do 39 | Utils.log_on_error do 40 | result = :atomics.get(atomic_ref, 1) 41 | {:reply, result, nil} 42 | end 43 | end 44 | 45 | @impl true 46 | def handle_cast({:put, atomic_ref, value}, _state) do 47 | Utils.log_on_error do 48 | :atomics.put(atomic_ref, 1, value) 49 | {:noreply, nil} 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/membrane/core/element/callback_context.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Core.Element.CallbackContext do 2 | @moduledoc false 3 | 4 | @type optional_fields :: 5 | [incoming_demand: non_neg_integer()] 6 | | [pad_options: map()] 7 | | [old_stream_format: Membrane.StreamFormat.t()] 8 | | [start_of_stream_received?: boolean()] 9 | 10 | @spec from_state(Membrane.Core.Element.State.t(), optional_fields()) :: 11 | Membrane.Element.CallbackContext.t() 12 | def from_state(state, optional_fields \\ []) do 13 | Map.new(optional_fields) 14 | |> Map.merge(%{ 15 | pads: state.pads_data, 16 | clock: state.synchronization.clock, 17 | parent_clock: state.synchronization.parent_clock, 18 | module: state.module, 19 | name: state.name, 20 | playback: state.playback, 21 | resource_guard: state.resource_guard, 22 | utility_supervisor: state.subprocess_supervisor 23 | }) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/membrane/core/element/diamond_detection_controller/diamond_detection_state.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Core.Element.DiamondDetectionController.DiamondDatectionState do 2 | @moduledoc false 3 | 4 | use Bunch.Access 5 | 6 | alias Membrane.Core.Element.DiamondDetectionController.PathInGraph 7 | 8 | defstruct ref_to_path: %{}, 9 | trigger_refs: MapSet.new(), 10 | postponed?: false 11 | 12 | @type t :: %__MODULE__{ 13 | ref_to_path: %{optional(reference()) => PathInGraph.t()}, 14 | trigger_refs: MapSet.t(reference()), 15 | postponed?: boolean() 16 | } 17 | end 18 | -------------------------------------------------------------------------------- /lib/membrane/core/element/diamond_detection_controller/diamond_logger.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Core.Element.DiamondDetectionController.DiamondLogger do 2 | @moduledoc false 3 | 4 | alias Membrane.Core.Element.DiamondDetectionController.PathInGraph 5 | alias Membrane.Core.Element.DiamondDetectionController.PathInGraph.Vertex 6 | 7 | require Membrane.Logger 8 | 9 | # logging a diamond is moved to the separate module due to testing 10 | 11 | @spec log_diamond(PathInGraph.t(), PathInGraph.t()) :: :ok 12 | def log_diamond(path_a, path_b) do 13 | from = List.last(path_a) |> Map.fetch!(:component_path) 14 | to = List.first(path_a) |> Map.fetch!(:component_path) 15 | 16 | Membrane.Logger.debug(""" 17 | Two paths from element #{from} to #{to} were detected, in which all pads are operating in pull \ 18 | mode. With such a pipeline configuration, the membrane flow control mechanism may stop demanding \ 19 | buffers. If you are debugging such an issue, keep in mind that input pads with `flow_control: :auto` \ 20 | demand data when there is a demand for data on ALL output pads with `flow_control: :auto`. 21 | 22 | The first path from #{from} to #{to} leads: 23 | #{inspect_path(path_a)} 24 | 25 | The second path from #{from} to #{to} leads: 26 | #{inspect_path(path_b)} 27 | """) 28 | 29 | :ok 30 | end 31 | 32 | defp inspect_path(path) do 33 | path 34 | |> Enum.reverse() 35 | |> Enum.chunk_every(2, 1, :discard) 36 | |> Enum.map_join("\n", fn [%Vertex{} = from, %Vertex{} = to] -> 37 | """ 38 | * from #{from.component_path} via output pad #{inspect(from.output_pad_ref)} to \ 39 | #{to.component_path} via input pad #{inspect(to.input_pad_ref)} 40 | """ 41 | end) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/membrane/core/element/diamond_detection_controller/path_in_grapth.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Core.Element.DiamondDetectionController.PathInGraph do 2 | @moduledoc false 3 | 4 | defmodule Vertex do 5 | @moduledoc false 6 | require Membrane.Pad, as: Pad 7 | 8 | defstruct [:pid, :component_path, :input_pad_ref, :output_pad_ref] 9 | 10 | @type t :: %__MODULE__{ 11 | pid: pid(), 12 | component_path: String.t(), 13 | input_pad_ref: Pad.ref() | nil, 14 | output_pad_ref: Pad.ref() | nil 15 | } 16 | end 17 | 18 | @type t :: [Vertex.t()] 19 | end 20 | -------------------------------------------------------------------------------- /lib/membrane/core/element/playback_queue.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Core.Element.PlaybackQueue do 2 | @moduledoc false 3 | 4 | alias Membrane.Core.Element.State 5 | 6 | @type t :: [(State.t() -> State.t())] 7 | 8 | @spec store((State.t() -> State.t()), State.t()) :: State.t() 9 | def store(function, %State{playback_queue: playback_queue} = state) do 10 | %State{state | playback_queue: [function | playback_queue]} 11 | end 12 | 13 | @spec eval(State.t()) :: State.t() 14 | def eval(%State{playback_queue: playback_queue} = state) do 15 | state = 16 | playback_queue 17 | |> List.foldr(state, fn function, state -> function.(state) end) 18 | 19 | %State{state | playback_queue: []} 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/membrane/core/events/end_of_stream.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Core.Events.EndOfStream do 2 | @moduledoc false 3 | # Sent by Membrane.Element.Action.end_of_stream/ 4 | # Invokes `c:Membrane.Element.WithInputPads.end_of_stream/3` callback. 5 | @derive Membrane.EventProtocol 6 | defstruct [] 7 | @type t :: %__MODULE__{} 8 | end 9 | -------------------------------------------------------------------------------- /lib/membrane/core/events/start_of_stream.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Core.Events.StartOfStream do 2 | @moduledoc false 3 | # Generated before processing the first buffer. 4 | # Invokes `c:Membrane.Element.WithInputPads.end_of_stream/3` callback. 5 | defstruct [] 6 | @type t :: %__MODULE__{} 7 | end 8 | 9 | defimpl Membrane.EventProtocol, for: Membrane.Core.Events.StartOfStream do 10 | use Membrane.EventProtocol.DefaultImpl 11 | @impl true 12 | def sticky?(_event), do: true 13 | end 14 | -------------------------------------------------------------------------------- /lib/membrane/core/filter_aggregator/action.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Core.FilterAggregator.InternalAction do 2 | @moduledoc false 3 | # Definitions of actions used internally by Membrane.FilterAggregator 4 | 5 | @type t :: {__MODULE__, atom()} | {__MODULE__, atom(), args :: any()} 6 | 7 | defmacro setup() do 8 | quote do: {unquote(__MODULE__), :setup} 9 | end 10 | 11 | defmacro playing() do 12 | quote do: {unquote(__MODULE__), :playing} 13 | end 14 | 15 | defmacro start_of_stream(pad) do 16 | quote do: {unquote(__MODULE__), :start_of_stream, unquote(pad)} 17 | end 18 | 19 | @doc """ 20 | An action allowing to manipulate the context passed to callbacks 21 | """ 22 | defmacro merge_context(map) do 23 | quote do: {unquote(__MODULE__), :merge_context, unquote(map)} 24 | end 25 | 26 | defguard is_internal_action(action) 27 | when is_tuple(action) and elem(action, 0) == unquote(__MODULE__) 28 | end 29 | -------------------------------------------------------------------------------- /lib/membrane/core/inspect.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Core.Inspect do 2 | @moduledoc false 3 | 4 | alias Inspect.Algebra 5 | alias Membrane.Core.Component 6 | 7 | @doc """ 8 | A function, that inspects passed state sorting its fields withing the order in which 9 | they occur in the list passed to `defstruct`. 10 | """ 11 | @spec inspect(Component.state(), Inspect.Opts.t()) :: Inspect.Algebra.t() 12 | def inspect(%state_module{} = state, opts) do 13 | ordered_fields = 14 | state_module.__info__(:struct) 15 | |> Enum.map(& &1.field) 16 | 17 | field_to_doc_fun = 18 | fn field, opts -> 19 | value_doc = 20 | Map.fetch!(state, field) 21 | |> Algebra.to_doc(opts) 22 | 23 | Algebra.concat("#{Atom.to_string(field)}: ", value_doc) 24 | end 25 | 26 | Algebra.container_doc( 27 | "%#{Kernel.inspect(state_module)}{", 28 | ordered_fields, 29 | "}", 30 | opts, 31 | field_to_doc_fun, 32 | break: :strict 33 | ) 34 | end 35 | end 36 | 37 | [Pipeline, Bin, Element] 38 | |> Enum.map(fn component -> 39 | state_module = Module.concat([Membrane.Core, component, State]) 40 | 41 | defimpl Inspect, for: state_module do 42 | @spec inspect(unquote(state_module).t(), Inspect.Opts.t()) :: Inspect.Algebra.t() 43 | defdelegate inspect(state, opts), to: Membrane.Core.Inspect 44 | end 45 | end) 46 | -------------------------------------------------------------------------------- /lib/membrane/core/lifecycle_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Core.LifecycleController do 2 | @moduledoc false 3 | 4 | alias Membrane.Core.{Component, Message, Parent} 5 | alias Membrane.SetupError 6 | 7 | require Membrane.Core.Message 8 | require Membrane.Logger 9 | 10 | @type setup_operation :: :incomplete | :complete 11 | 12 | @spec handle_setup_operation(setup_operation(), Component.state()) :: 13 | Component.state() 14 | def handle_setup_operation(operation, state) do 15 | :ok = assert_operation_allowed!(operation, state.setup_incomplete_returned?) 16 | 17 | cond do 18 | operation == :incomplete -> 19 | Membrane.Logger.debug("Component deferred initialization") 20 | %{state | setup_incomplete_returned?: true} 21 | 22 | Component.pipeline?(state) -> 23 | # complete_setup/1 will be called in Membrane.Core.Pipeline.ActionHandler.handle_end_of_actions/1 24 | %{state | awaiting_setup_completition?: true} 25 | 26 | Component.child?(state) -> 27 | complete_setup(state) 28 | end 29 | end 30 | 31 | @spec complete_setup(Component.state()) :: Component.state() 32 | def complete_setup(state) do 33 | state = %{state | initialized?: true, setup_incomplete_returned?: false} 34 | Membrane.Logger.debug("Component initialized") 35 | 36 | cond do 37 | Component.pipeline?(state) -> 38 | Parent.LifecycleController.handle_playing(state) 39 | 40 | Component.child?(state) -> 41 | Message.send(state.parent_pid, :initialized, state.name) 42 | state 43 | end 44 | end 45 | 46 | @spec assert_operation_allowed!(setup_operation(), boolean()) :: :ok | no_return() 47 | defp assert_operation_allowed!(:incomplete, true) do 48 | raise SetupError, """ 49 | Action {:setup, :incomplete} was returned more than once 50 | """ 51 | end 52 | 53 | defp assert_operation_allowed!(:complete, false) do 54 | raise SetupError, """ 55 | Action {:setup, :complete} was returned, but setup is already completed 56 | """ 57 | end 58 | 59 | defp assert_operation_allowed!(operation, _status) 60 | when operation not in [:incomplete, :complete] do 61 | raise SetupError, """ 62 | Action {:setup, #{inspect(operation)}} was returned, but second element in the tuple must 63 | be :complete or :incomplete 64 | """ 65 | end 66 | 67 | defp assert_operation_allowed!(_operation, _status), do: :ok 68 | end 69 | -------------------------------------------------------------------------------- /lib/membrane/core/message.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Core.Message do 2 | @moduledoc false 3 | 4 | # Record representing membrane internal message 5 | 6 | alias Membrane.Pad 7 | 8 | require Record 9 | 10 | Record.defrecord(:message, __MODULE__, type: nil, args: [], opts: []) 11 | 12 | @type t :: {__MODULE__, type, args, opts} 13 | @type type :: atom 14 | @type args :: list | any 15 | @type opts :: Keyword.t() 16 | 17 | defmacro new(type, args \\ [], opts \\ []) do 18 | quote do 19 | unquote(__MODULE__).message(type: unquote(type), args: unquote(args), opts: unquote(opts)) 20 | end 21 | end 22 | 23 | @spec send(pid, type, args, opts) :: :ok 24 | def send(pid, type, args \\ [], opts \\ []) do 25 | Kernel.send(pid, message(type: type, args: args, opts: opts)) 26 | :ok 27 | end 28 | 29 | @spec self(type, args, opts) :: :ok 30 | def self(type, args \\ [], opts \\ []) do 31 | __MODULE__.send(self(), type, args, opts) 32 | :ok 33 | end 34 | 35 | @spec call(GenServer.server(), type, args, opts, timeout()) :: 36 | term() | {:error, {:call_failure, any}} 37 | def call(pid, type, args \\ [], opts \\ [], timeout \\ 5000) do 38 | try do 39 | GenServer.call(pid, message(type: type, args: args, opts: opts), timeout) 40 | catch 41 | :exit, reason -> 42 | {:error, {:call_failure, reason}} 43 | end 44 | end 45 | 46 | @spec call!(GenServer.server(), type, args, opts, timeout()) :: term() 47 | def call!(pid, type, args \\ [], opts \\ [], timeout \\ 5000) do 48 | GenServer.call(pid, message(type: type, args: args, opts: opts), timeout) 49 | end 50 | 51 | @spec for_pad(t()) :: Pad.ref() 52 | def for_pad(message(opts: opts)), do: Keyword.get(opts, :for_pad) 53 | 54 | @spec set_for_pad(t(), Pad.ref()) :: t() 55 | def set_for_pad(message(opts: opts) = msg, pad), 56 | do: message(msg, opts: Keyword.put(opts, :for_pad, pad)) 57 | end 58 | -------------------------------------------------------------------------------- /lib/membrane/core/parent.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Core.Parent do 2 | @moduledoc false 3 | 4 | @type state :: Membrane.Core.Bin.State.t() | Membrane.Core.Pipeline.State.t() 5 | 6 | @spec check_deprecated_callbacks(Macro.Env.t(), binary) :: :ok 7 | def check_deprecated_callbacks(env, _bytecode) do 8 | modules_whitelist = [Membrane.Testing.Pipeline] 9 | 10 | if env.module not in modules_whitelist and 11 | Module.defines?(env.module, {:handle_spec_started, 3}, :def) do 12 | warn_message = """ 13 | Callback handle_spec_started/3 has been deprecated since \ 14 | :membrane_core v1.1.0, but it is implemented in #{inspect(env.module)} 15 | """ 16 | 17 | IO.warn(warn_message, []) 18 | end 19 | 20 | :ok 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/membrane/core/parent/child_entry_parser.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Core.Parent.ChildEntryParser do 2 | @moduledoc false 3 | 4 | alias Membrane.{ChildEntry, ChildrenSpec, ParentError} 5 | 6 | @type raw_child_entry :: %ChildEntry{ 7 | name: Membrane.Child.name(), 8 | module: module, 9 | options: struct | nil, 10 | component_type: :element | :bin 11 | } 12 | 13 | @spec parse([ChildrenSpec.Builder.child_spec()]) :: 14 | [raw_child_entry] | no_return 15 | def parse(children_spec) do 16 | Enum.map(children_spec, &parse_child/1) 17 | end 18 | 19 | defp parse_child({name, %module{} = struct, _options}) do 20 | %ChildEntry{ 21 | name: name, 22 | module: module, 23 | options: struct, 24 | component_type: component_type(module) 25 | } 26 | end 27 | 28 | defp parse_child({name, module, _options}) when is_atom(module) do 29 | struct = module |> Bunch.Module.struct() 30 | 31 | %ChildEntry{ 32 | name: name, 33 | module: module, 34 | options: struct, 35 | component_type: component_type(module) 36 | } 37 | end 38 | 39 | defp parse_child(config) do 40 | raise ParentError, "Invalid children config: #{inspect(config, pretty: true)}" 41 | end 42 | 43 | defp component_type(module) do 44 | cond do 45 | Membrane.Element.element?(module) -> :element 46 | Membrane.Bin.bin?(module) -> :bin 47 | true -> raise ParentError, not_child: module 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/membrane/core/parent/children_model.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Core.Parent.ChildrenModel do 2 | @moduledoc false 3 | 4 | alias Membrane.{Child, ChildEntry, UnknownChildError} 5 | alias Membrane.Core.Parent 6 | 7 | @type children :: %{Child.name() => ChildEntry.t()} 8 | 9 | @spec assert_child_exists!(Parent.state(), Child.name()) :: :ok 10 | def assert_child_exists!(state, child) do 11 | _data = get_child_data!(state, child) 12 | :ok 13 | end 14 | 15 | @spec get_child_data!(Parent.state(), Child.name()) :: ChildEntry.t() 16 | def get_child_data!(%{children: children}, child) do 17 | case children do 18 | %{^child => data} -> data 19 | _children -> raise UnknownChildError, name: child, children: children 20 | end 21 | end 22 | 23 | @spec update_children!(Parent.state(), [Child.name()], (ChildEntry.t() -> ChildEntry.t())) :: 24 | Parent.state() 25 | def update_children!(%{children: children} = state, children_names, mapper) do 26 | children = 27 | Enum.reduce(children_names, children, fn name, children -> 28 | case children do 29 | %{^name => data} -> %{children | name => mapper.(data)} 30 | _children -> raise UnknownChildError, name: name, children: children 31 | end 32 | end) 33 | 34 | %{state | children: children} 35 | end 36 | 37 | @spec update_children(Parent.state(), (ChildEntry.t() -> ChildEntry.t())) :: Parent.state() 38 | def update_children(%{children: children} = state, mapper) do 39 | children = Map.new(children, fn {name, entry} -> {name, mapper.(entry)} end) 40 | %{state | children: children} 41 | end 42 | 43 | @spec delete_child(Parent.state(), Child.name()) :: Parent.state() 44 | def delete_child(%{children: children} = state, child) do 45 | %{state | children: Map.delete(children, child)} 46 | end 47 | 48 | @spec all?(Parent.state(), (ChildEntry.t() -> as_boolean(term))) :: boolean() 49 | def all?(state, predicate) do 50 | state.children 51 | |> Enum.all?(fn {_k, v} -> predicate.(v) end) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/membrane/core/parent/clock_handler.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Core.Parent.ClockHandler do 2 | @moduledoc false 3 | 4 | alias Membrane.{Clock, Core, ParentError} 5 | alias Membrane.Core.Parent.ChildEntryParser 6 | 7 | @spec choose_clock( 8 | [ChildEntryParser.raw_child_entry()], 9 | Membrane.Child.name() | nil, 10 | Core.Parent.state() 11 | ) :: 12 | Core.Parent.state() | no_return 13 | def choose_clock(_children, nil, state) do 14 | state 15 | end 16 | 17 | def choose_clock(children, provider, state) do 18 | %{synchronization: synchronization} = state 19 | 20 | components = 21 | case state do 22 | %Core.Bin.State{} -> [%{name: Membrane.Parent, clock: synchronization.parent_clock}] 23 | %Core.Pipeline.State{} -> [] 24 | end 25 | 26 | components = components ++ children 27 | clock = get_clock_from_provider(components, provider) 28 | set_clock_provider(clock, state) 29 | end 30 | 31 | @spec reset_clock(Core.Parent.state()) :: Core.Parent.state() 32 | def reset_clock(state), 33 | do: set_clock_provider(%{clock: nil, provider: nil}, state) 34 | 35 | defp set_clock_provider(clock_provider, state) do 36 | Clock.proxy_for(state.synchronization.clock_proxy, clock_provider.clock) 37 | put_in(state, [:synchronization, :clock_provider], clock_provider) 38 | end 39 | 40 | defp get_clock_from_provider(components, provider) do 41 | components 42 | |> Enum.find(&(&1.name == provider)) 43 | |> case do 44 | nil -> 45 | raise ParentError, "Unknown clock provider: #{inspect(provider)}" 46 | 47 | %{clock: nil} -> 48 | raise ParentError, "#{inspect(provider)} is not a clock provider" 49 | 50 | %{clock: clock} -> 51 | %{clock: clock, provider: provider} 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/membrane/core/parent/crash_group.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Core.Parent.CrashGroup do 2 | @moduledoc false 3 | 4 | # A module representing crash group: 5 | # * name - name that identifies the group 6 | # * type - responsible for restart policy of members of groups 7 | # * members - list of members of group 8 | # * reason - reason of the crash 9 | 10 | use Bunch.Access 11 | 12 | @type name() :: any() 13 | 14 | @type t :: %__MODULE__{ 15 | name: name(), 16 | mode: :temporary, 17 | members: [Membrane.Child.name()], 18 | detonating?: boolean(), 19 | crash_initiator: Membrane.Child.name(), 20 | crash_reason: :normal | :shutdown | {:shutdown, term()} | term() 21 | } 22 | 23 | @enforce_keys [:name, :mode] 24 | defstruct @enforce_keys ++ 25 | [members: [], detonating?: false, crash_initiator: nil, crash_reason: nil] 26 | end 27 | -------------------------------------------------------------------------------- /lib/membrane/core/parent/diamond_detection_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Core.Parent.DiamondDetectionController do 2 | @moduledoc false 3 | 4 | alias Membrane.Child 5 | alias Membrane.Core.Parent 6 | 7 | require Membrane.Core.Message, as: Message 8 | 9 | @spec start_diamond_detection_trigger(Child.name(), reference(), Parent.state()) :: :ok 10 | def start_diamond_detection_trigger(child_name, trigger_ref, state) do 11 | with %{component_type: :element, pid: pid} <- state.children[child_name] do 12 | message = %{type: :start_trigger, ref: trigger_ref} 13 | Message.send(pid, :diamond_detection, message) 14 | end 15 | 16 | :ok 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/membrane/core/parent/link.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Core.Parent.Link do 2 | @moduledoc false 3 | 4 | use Bunch.Access 5 | 6 | alias __MODULE__.Endpoint 7 | 8 | @enforce_keys [:id, :from, :to] 9 | defstruct @enforce_keys ++ [linked?: false, spec_ref: nil] 10 | 11 | @type id :: reference() 12 | 13 | @type t :: %__MODULE__{ 14 | id: id(), 15 | from: Endpoint.t(), 16 | to: Endpoint.t(), 17 | linked?: boolean(), 18 | spec_ref: Membrane.Core.Parent.ChildLifeController.spec_ref() 19 | } 20 | end 21 | -------------------------------------------------------------------------------- /lib/membrane/core/parent/link_endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Core.Parent.Link.Endpoint do 2 | @moduledoc false 3 | 4 | use Bunch.Access 5 | 6 | alias Membrane.{Child, Pad} 7 | 8 | @enforce_keys [:child, :pad_spec] 9 | defstruct @enforce_keys ++ 10 | [pad_ref: nil, pid: nil, pad_props: [], pad_info: %{}, child_spec_ref: nil] 11 | 12 | @type t() :: %__MODULE__{ 13 | child: Child.name() | {Membrane.Bin, :itself}, 14 | pad_spec: Pad.name() | Pad.ref(), 15 | pad_ref: Pad.ref(), 16 | pid: pid(), 17 | pad_props: map(), 18 | pad_info: map() 19 | } 20 | end 21 | -------------------------------------------------------------------------------- /lib/membrane/core/parent/specification_parser.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Core.Parent.SpecificationParser do 2 | @moduledoc false 3 | use Bunch 4 | 5 | alias Membrane.Core.Parent.Link 6 | alias Membrane.Core.Parent.Link.Endpoint 7 | alias Membrane.{ChildrenSpec, Element, Pad, ParentError} 8 | 9 | require Membrane.Logger 10 | 11 | @type raw_link :: %Link{from: raw_endpoint(), to: raw_endpoint()} 12 | 13 | @type raw_endpoint :: %Endpoint{ 14 | child: Element.name() | {Membrane.Bin, :itself}, 15 | pad_spec: Pad.name() | Pad.ref(), 16 | pad_ref: Pad.ref() | nil, 17 | pid: pid() | nil, 18 | pad_props: map() 19 | } 20 | 21 | @spec parse([ChildrenSpec.builder()]) :: 22 | {[ChildrenSpec.Builder.child_spec()], [raw_link]} | no_return 23 | def parse(specifications) when is_list(specifications) do 24 | {children, links} = 25 | specifications 26 | |> List.flatten() 27 | |> Enum.map(fn 28 | %ChildrenSpec.Builder{links: links, children: children, status: :done} = builder -> 29 | if links == [] and children == [] do 30 | Membrane.Logger.warning( 31 | "The specification you have passed: #{builder} has no effect - it doesn't produce any children nor links." 32 | ) 33 | end 34 | 35 | {Enum.reverse(children), Enum.reverse(links)} 36 | 37 | _other -> 38 | from_spec_error(specifications) 39 | end) 40 | |> Enum.unzip() 41 | 42 | links = 43 | links 44 | |> List.flatten() 45 | |> Enum.filter(fn link -> Map.has_key?(link, :from_pad) end) 46 | |> Enum.map(fn %{} = link -> 47 | %Link{ 48 | id: make_ref(), 49 | from: %Endpoint{ 50 | child: link.from, 51 | pad_spec: link.from_pad, 52 | pad_props: link.from_pad_props 53 | }, 54 | to: %Endpoint{ 55 | child: link.to, 56 | pad_spec: link.to_pad, 57 | pad_props: link.to_pad_props 58 | } 59 | } 60 | end) 61 | 62 | children = children |> List.flatten() 63 | {children, links} 64 | end 65 | 66 | def parse(specification), do: from_spec_error([specification]) 67 | 68 | @spec from_spec_error([ChildrenSpec.builder()]) :: no_return 69 | defp from_spec_error(specifications) do 70 | raise ParentError, """ 71 | Invalid specifications: #{inspect(specifications)}. The link lacks it destination. 72 | See `#{inspect(ChildrenSpec)}` for information on how to specify children and links 73 | beween them. 74 | """ 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/membrane/core/pipeline/callback_context.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Core.Pipeline.CallbackContext do 2 | @moduledoc false 3 | 4 | @type optional_fields :: 5 | [from: GenServer.from()] 6 | | [ 7 | members: [Membrane.Child.name()], 8 | crash_initiator: Membrane.Child.name(), 9 | crash_reason: :normal | :shutdown | {:shutdown, term()} | term() 10 | ] 11 | | [start_of_stream_received?: boolean()] 12 | | [ 13 | group_name: Membrane.Child.group() | nil, 14 | crash_initiator: Membrane.Child.name() | nil, 15 | exit_reason: :normal | :shutdown | {:shutdown, term()} | term() 16 | ] 17 | 18 | @spec from_state(Membrane.Core.Pipeline.State.t(), optional_fields()) :: 19 | Membrane.Pipeline.CallbackContext.t() 20 | def from_state(state, optional_fields \\ []) do 21 | Map.new(optional_fields) 22 | |> Map.merge(%{ 23 | clock: state.synchronization.clock_proxy, 24 | children: state.children, 25 | module: state.module, 26 | playback: state.playback, 27 | resource_guard: state.resource_guard, 28 | utility_supervisor: state.subprocess_supervisor 29 | }) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/membrane/core/pipeline/state.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Core.Pipeline.State do 2 | @moduledoc false 3 | 4 | # Structure representing state of a pipeline. It is a part of the private API. 5 | # It does not represent state of pipelines you construct, it's a state used 6 | # internally in Membrane. 7 | 8 | use Bunch 9 | use Bunch.Access 10 | 11 | alias Membrane.Child 12 | alias Membrane.Core.Parent.{ChildLifeController, ChildrenModel, CrashGroup, Link} 13 | alias Membrane.Core.Timer 14 | 15 | @type t :: %__MODULE__{ 16 | module: module(), 17 | playback: Membrane.Playback.t(), 18 | internal_state: Membrane.Pipeline.state() | nil, 19 | children: ChildrenModel.children(), 20 | links: %{Link.id() => Link.t()}, 21 | crash_groups: %{CrashGroup.name() => CrashGroup.t()}, 22 | pending_specs: ChildLifeController.pending_specs(), 23 | synchronization: %{ 24 | timers: %{Timer.id() => Timer.t()}, 25 | clock_provider: %{ 26 | clock: Membrane.Clock.t() | nil, 27 | provider: Child.name() | nil 28 | }, 29 | clock_proxy: Membrane.Clock.t() 30 | }, 31 | initialized?: boolean(), 32 | terminating?: boolean(), 33 | resource_guard: Membrane.ResourceGuard.t(), 34 | setup_incomplete_returned?: boolean(), 35 | stalker: Membrane.Core.Stalker.t(), 36 | subprocess_supervisor: pid(), 37 | awaiting_setup_completition?: boolean() 38 | } 39 | 40 | # READ THIS BEFORE ADDING NEW FIELD!!! 41 | 42 | # Fields of this structure will be inspected in the same order, in which they occur in the 43 | # list passed to `defstruct`. Take a look at lib/membrane/core/inspect.ex to get more info. 44 | # If you want to add a new field to the state, place it at the spot corresponding to its 45 | # importance and possibly near other related fields. 46 | 47 | defstruct module: nil, 48 | playback: :stopped, 49 | internal_state: nil, 50 | children: %{}, 51 | links: %{}, 52 | crash_groups: %{}, 53 | pending_specs: %{}, 54 | synchronization: nil, 55 | initialized?: false, 56 | terminating?: false, 57 | setup_incomplete_returned?: false, 58 | stalker: nil, 59 | resource_guard: nil, 60 | subprocess_supervisor: nil, 61 | awaiting_setup_completition?: false 62 | end 63 | -------------------------------------------------------------------------------- /lib/membrane/core/pipeline/zombie.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Core.Pipeline.Zombie do 2 | @moduledoc false 3 | # When a pipeline returns Membrane.Pipeline.Action.terminate() 4 | # and becomes a zombie-ie-ie oh oh oh oh oh oh oh, ay, oh, ya ya 5 | # this module is used to replace the user implementation of the pipeline 6 | use Membrane.Pipeline 7 | require Membrane.Logger 8 | 9 | # Overrides all the overridable callbacks to add a debug message that the original 10 | # implementation is not called 11 | Membrane.Pipeline.behaviour_info(:callbacks) 12 | |> Enum.filter(&Module.overridable?(__MODULE__, &1)) 13 | |> Enum.map(fn {name, arity} -> 14 | args = Enum.map(1..arity//1, &Macro.var(:"arg#{&1}", __MODULE__)) 15 | 16 | @impl true 17 | def unquote(name)(unquote_splicing(args)) do 18 | Membrane.Logger.debug( 19 | "Not calling the #{unquote(name)} callback with the following arguments: 20 | #{Enum.map_join(unquote(args), ", ", &inspect/1)} 21 | because the pipeline is in the zombie mode" 22 | ) 23 | 24 | super(unquote_splicing(args)) 25 | end 26 | end) 27 | end 28 | -------------------------------------------------------------------------------- /lib/membrane/core/process_helper.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Core.ProcessHelper do 2 | @moduledoc false 3 | require Membrane.Logger 4 | 5 | defguardp is_shutdown_tuple(reason) 6 | when is_tuple(reason) and tuple_size(reason) == 2 and elem(reason, 0) == :shutdown 7 | 8 | defguard is_silent_exit_reason(reason) 9 | when reason in [:normal, :shutdown] or is_shutdown_tuple(reason) 10 | 11 | @doc """ 12 | This is a hack to exit with a custom reason, but without having GenServer 13 | exit logs occurring when the exit reason is neither :normal, :shutdown 14 | nor {:shutdown, reason} 15 | """ 16 | @spec notoelo(any(), log?: boolean()) :: no_return() 17 | def notoelo(reason, opts \\ []) 18 | 19 | def notoelo(reason, log?: false) do 20 | do_notoelo(reason) 21 | end 22 | 23 | def notoelo(reason, _opts) when is_silent_exit_reason(reason) do 24 | do_notoelo(reason) 25 | end 26 | 27 | def notoelo(reason, _opts) do 28 | Membrane.Logger.error("Terminating with reason: #{inspect(reason)}") 29 | do_notoelo(reason) 30 | end 31 | 32 | defp do_notoelo(reason) do 33 | Process.flag(:trap_exit, false) 34 | Process.exit(self(), reason) 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/membrane/core/timer.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Core.Timer do 2 | @moduledoc false 3 | alias Membrane.Clock 4 | alias Membrane.Core.Message 5 | alias Membrane.Time 6 | 7 | require Membrane.Core.Message 8 | 9 | @type id :: any() 10 | @type interval :: Ratio.t() | Time.non_neg() | :no_interval 11 | @type t :: %__MODULE__{ 12 | id: id, 13 | interval: interval, 14 | init_time: Time.t(), 15 | clock: Clock.t(), 16 | next_tick_time: Time.t(), 17 | ratio: Clock.ratio(), 18 | timer_ref: reference() | nil, 19 | awaiting_message?: boolean() 20 | } 21 | 22 | @enforce_keys [:interval, :clock, :init_time, :id] 23 | defstruct @enforce_keys ++ 24 | [next_tick_time: 0, ratio: Ratio.new(1), timer_ref: nil, awaiting_message?: false] 25 | 26 | @spec start(id, interval, Clock.t()) :: t 27 | def start(id, interval, clock) do 28 | %__MODULE__{id: id, interval: interval, init_time: Time.monotonic_time(), clock: clock} 29 | |> tick 30 | end 31 | 32 | @spec stop(t) :: :ok 33 | def stop(%__MODULE__{interval: :no_interval}) do 34 | :ok 35 | end 36 | 37 | def stop(%__MODULE__{timer_ref: timer_ref}) do 38 | _time_left = Process.cancel_timer(timer_ref) 39 | :ok 40 | end 41 | 42 | @spec update_ratio(t, Clock.ratio()) :: t 43 | def update_ratio(timer, ratio) do 44 | %__MODULE__{timer | ratio: ratio} 45 | end 46 | 47 | @spec handle_message_arrived(t) :: t 48 | def handle_message_arrived(%__MODULE__{awaiting_message?: true} = timer) do 49 | %{timer | awaiting_message?: false} 50 | end 51 | 52 | @spec tick(t) :: t 53 | def tick(%__MODULE__{} = timer) 54 | when timer.awaiting_message? or timer.interval == :no_interval do 55 | timer 56 | end 57 | 58 | def tick(timer) do 59 | %__MODULE__{ 60 | id: id, 61 | interval: interval, 62 | init_time: init_time, 63 | next_tick_time: next_tick_time, 64 | ratio: ratio 65 | } = timer 66 | 67 | next_tick_time = Ratio.add(Ratio.new(next_tick_time), Ratio.new(interval)) 68 | 69 | # Next tick time converted to BEAM clock time 70 | beam_next_tick_time = 71 | Ratio.add(Ratio.new(init_time), Ratio.div(next_tick_time, ratio)) 72 | |> Ratio.floor() 73 | |> Time.as_milliseconds(:round) 74 | 75 | timer_ref = 76 | Process.send_after(self(), Message.new(:timer_tick, id), beam_next_tick_time, abs: true) 77 | 78 | %__MODULE__{ 79 | timer 80 | | next_tick_time: next_tick_time |> Ratio.floor(), 81 | timer_ref: timer_ref, 82 | awaiting_message?: true 83 | } 84 | end 85 | 86 | @spec set_interval(t, interval) :: t 87 | def set_interval(%__MODULE__{interval: :no_interval} = timer, interval) do 88 | %__MODULE__{timer | interval: interval} 89 | |> tick() 90 | end 91 | 92 | def set_interval(timer, interval) do 93 | %__MODULE__{timer | interval: interval} 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /lib/membrane/core/utils.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Core.Utils do 2 | @moduledoc false 3 | 4 | # For some reason GenServer processes sometimes don't print logs about crash, so 5 | # we add this macro, to ensure that error logs are always printed 6 | defmacro log_on_error(do: code) do 7 | error_source = 8 | case __CALLER__.module do 9 | Membrane.Core.Element -> "Membrane Element" 10 | Membrane.Core.Bin -> "Membrane Bin" 11 | Membrane.Core.Pipeline -> "Membrane Pipeline" 12 | other -> inspect(other) 13 | end 14 | 15 | quote do 16 | try do 17 | unquote(code) 18 | rescue 19 | error -> 20 | require Membrane.Logger 21 | 22 | Membrane.Logger.error(""" 23 | Error occured in #{unquote(error_source)}: 24 | #{Exception.format(:error, error, __STACKTRACE__)} 25 | """) 26 | 27 | reraise error, __STACKTRACE__ 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/membrane/debug/filter.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Debug.Filter do 2 | @moduledoc """ 3 | Membrane Filter, that can be used to create a child that will be used to debug data flowing thouth pipeline. 4 | 5 | Any buffers, stream formats and events arriving to #{__MODULE__} will be forwarded by it to the opposite 6 | side than the one from which they came. 7 | 8 | Usage example: 9 | ```elixir 10 | child(:source, CustomSource) 11 | |> child(:filter, %Membrane.Debug.Filter{ 12 | handle_buffer: &IO.inspect(&1, label: "buffer"), 13 | handle_stream_format: &IO.inspect(&1, label: "stream format") 14 | }) 15 | |> child(:sink, CustomSink) 16 | ``` 17 | """ 18 | 19 | use Membrane.Filter 20 | 21 | alias Membrane.Buffer 22 | alias Membrane.Event 23 | alias Membrane.StreamFormat 24 | 25 | def_input_pad :input, accepted_format: _any, flow_control: :auto 26 | def_output_pad :output, accepted_format: _any, flow_control: :auto 27 | 28 | @spec noop(any()) :: :ok 29 | def noop(_arg), do: :ok 30 | 31 | def_options handle_buffer: [ 32 | spec: (Buffer.t() -> any()), 33 | default: &__MODULE__.noop/1, 34 | description: """ 35 | Function with arity 1, that will be called with all buffers handled by this sink. 36 | Result of this function is ignored. 37 | """ 38 | ], 39 | handle_event: [ 40 | spec: (Event.t() -> any()), 41 | default: &__MODULE__.noop/1, 42 | description: """ 43 | Function with arity 1, that will be called with all events handled by this sink. 44 | Result of this function is ignored. 45 | """ 46 | ], 47 | handle_stream_format: [ 48 | spec: (StreamFormat.t() -> any()), 49 | default: &__MODULE__.noop/1, 50 | description: """ 51 | Function with arity 1, that will be called with all stream formats handled by this sink. 52 | Result of this function is ignored. 53 | """ 54 | ] 55 | 56 | @impl true 57 | def handle_init(_ctx, opts) do 58 | {[], Map.from_struct(opts)} 59 | end 60 | 61 | @impl true 62 | def handle_buffer(:input, buffer, _ctx, state) do 63 | _ingored = state.handle_buffer.(buffer) 64 | {[buffer: {:output, buffer}], state} 65 | end 66 | 67 | @impl true 68 | def handle_event(_pad, event, _ctx, state) do 69 | _ingored = state.handle_event.(event) 70 | {[forward: event], state} 71 | end 72 | 73 | @impl true 74 | def handle_stream_format(:input, stream_format, _ctx, state) do 75 | _ingored = state.handle_stream_format.(stream_format) 76 | {[stream_format: {:output, stream_format}], state} 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/membrane/debug/sink.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Debug.Sink do 2 | @moduledoc """ 3 | Membrane Sink, that can be used to create a child that will be used to debug data flowing thouth pipeline. 4 | 5 | Usage example: 6 | ```elixir 7 | child(:source, CustomSource) 8 | |> child(:sink, %Membrane.Debug.Sink{ 9 | handle_buffer: &IO.inspect(&1, label: "buffer"), 10 | handle_event: &IO.inspect(&1, label: "event") 11 | }) 12 | ``` 13 | """ 14 | 15 | use Membrane.Sink 16 | 17 | alias Membrane.Buffer 18 | alias Membrane.Event 19 | alias Membrane.StreamFormat 20 | 21 | def_input_pad :input, accepted_format: _any, flow_control: :auto 22 | 23 | @spec noop(any()) :: :ok 24 | def noop(_arg), do: :ok 25 | 26 | def_options handle_buffer: [ 27 | spec: (Buffer.t() -> any()), 28 | default: &__MODULE__.noop/1, 29 | description: """ 30 | Function with arity 1, that will be called with all buffers handled by this sink. 31 | Result of this function is ignored. 32 | """ 33 | ], 34 | handle_event: [ 35 | spec: (Event.t() -> any()), 36 | default: &__MODULE__.noop/1, 37 | description: """ 38 | Function with arity 1, that will be called with all events handled by this sink. 39 | Result of this function is ignored. 40 | """ 41 | ], 42 | handle_stream_format: [ 43 | spec: (StreamFormat.t() -> any()), 44 | default: &__MODULE__.noop/1, 45 | description: """ 46 | Function with arity 1, that will be called with all stream formats handled by this sink. 47 | Result of this function is ignored. 48 | """ 49 | ] 50 | 51 | @impl true 52 | def handle_init(_ctx, opts) do 53 | {[], Map.from_struct(opts)} 54 | end 55 | 56 | @impl true 57 | def handle_buffer(:input, buffer, _ctx, state) do 58 | _ignored = state.handle_buffer.(buffer) 59 | {[], state} 60 | end 61 | 62 | @impl true 63 | def handle_event(:input, event, _ctx, state) do 64 | _ignored = state.handle_event.(event) 65 | {[], state} 66 | end 67 | 68 | @impl true 69 | def handle_stream_format(:input, stream_format, _ctx, state) do 70 | _ignored = state.handle_stream_format.(stream_format) 71 | {[], state} 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/membrane/element.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Element do 2 | @moduledoc """ 3 | Module containing types and functions for operating on elements. 4 | 5 | For behaviours for elements check `Membrane.Source`, `Membrane.Filter`, 6 | `Membrane.Endpoint` and `Membrane.Sink`. 7 | 8 | ## Behaviours 9 | Element-specific behaviours are specified in modules: 10 | - `Membrane.Element.WithOutputPads` - behaviour common to sources, 11 | filters and endpoints 12 | - `Membrane.Element.WithInputPads` - behaviour common to sinks, 13 | filters and endpoints 14 | - Base modules (`Membrane.Source`, `Membrane.Filter`, `Membrane.Endpoint`, 15 | `Membrane.Sink`) - behaviours specific to each element type. 16 | 17 | ## Callbacks 18 | Modules listed above provide specifications of callbacks that define elements 19 | lifecycle. All of these callbacks have names with the `handle_` prefix. 20 | They are used to define reaction to certain events that happen during runtime, 21 | and indicate what actions framework should undertake as a result, besides 22 | executing element-specific code. 23 | 24 | For actions that can be returned by each callback, see `Membrane.Element.Action` 25 | module. 26 | """ 27 | 28 | @typedoc """ 29 | Defines options that can be received in `c:Membrane.Element.Base.handle_init/2` 30 | callback. 31 | """ 32 | @type options :: struct | nil 33 | 34 | @typedoc """ 35 | Type that defines an element name by which it is identified. 36 | """ 37 | @type name :: tuple() | atom() 38 | 39 | @typedoc """ 40 | Defines possible element types: 41 | - source, producing buffers 42 | - filter, processing buffers 43 | - endpoint, producing and consuming buffers 44 | - sink, consuming buffers 45 | """ 46 | @type type :: :source | :filter | :endpoint | :sink 47 | 48 | @typedoc """ 49 | Type of user-managed state of element. 50 | """ 51 | @type state :: any() 52 | 53 | @doc """ 54 | Checks whether module is an element. 55 | """ 56 | @spec element?(module) :: boolean 57 | def element?(module) do 58 | module |> Bunch.Module.check_behaviour(:membrane_element?) 59 | end 60 | 61 | defguard is_element_name?(arg) when is_atom(arg) or is_tuple(arg) 62 | end 63 | -------------------------------------------------------------------------------- /lib/membrane/element/callback_context.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Element.CallbackContext do 2 | @moduledoc """ 3 | Describes context passed to the Membrane Element callbacks. 4 | """ 5 | 6 | @typedoc """ 7 | Type describing context passed to the Membrane Element callbacks. 8 | 9 | Field `:incoming_demand` is present only in 10 | `c:Membrane.Element.WithOutputPads.handle_demand/5`. 11 | 12 | Field `:pad_options` is present only in `c:Membrane.Element.Base.handle_pad_added/3` 13 | and `c:Membrane.Element.Base.handle_pad_removed/3`. 14 | 15 | Field `:start_of_stream_received?` is present only in 16 | `c:Membrane.Element.WithInputPads.handle_end_of_stream/3`. 17 | 18 | Field `:old_stream_format` is present only in 19 | `c:Membrane.Element.WithInputPads.handle_stream_format/4`. 20 | """ 21 | @type t :: %{ 22 | :clock => Membrane.Clock.t() | nil, 23 | :module => module(), 24 | :name => Membrane.Element.name(), 25 | :pads => %{Membrane.Pad.ref() => Membrane.Element.PadData.t()}, 26 | :parent_clock => Membrane.Clock.t() | nil, 27 | :playback => Membrane.Playback.t(), 28 | :resource_guard => Membrane.ResourceGuard.t(), 29 | :utility_supervisor => Membrane.UtilitySupervisor.t(), 30 | optional(:incoming_demand) => non_neg_integer(), 31 | optional(:pad_options) => map(), 32 | optional(:old_stream_format) => Membrane.StreamFormat.t(), 33 | optional(:start_of_stream_received?) => boolean() 34 | } 35 | end 36 | -------------------------------------------------------------------------------- /lib/membrane/element/with_output_pads.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Element.WithOutputPads do 2 | @moduledoc """ 3 | Module defining behaviour for source and filter elements. 4 | 5 | When used declares behaviour implementation, provides default callback definitions 6 | and imports macros. 7 | 8 | For more information on implementing elements, see `Membrane.Element.Base`. 9 | """ 10 | 11 | alias Membrane.{Buffer, Element, Pad} 12 | alias Membrane.Core.Child.PadsSpecs 13 | alias Membrane.Element.CallbackContext 14 | 15 | @doc """ 16 | Callback called when buffers should be emitted by a source, filter or endpoint. 17 | 18 | It is called only for output pads in the `:manual` flow control mode, as in their case demand 19 | is triggered by the input pad of the subsequent element. 20 | 21 | In sources and endpoint, appropriate amount of data should be sent here. 22 | 23 | In filters, this callback should usually return `:demand` action with 24 | size sufficient for supplying incoming demand. This will result in calling 25 | `c:Membrane.WithInputPads.handle_buffer/4`, which is to supply 26 | the demand. 27 | 28 | If a source or an endpoint is unable to produce enough buffers, or a filter 29 | underestimated returned demand, the `:redemand` action should be used (see 30 | `t:Membrane.Element.Action.redemand/0`). 31 | 32 | Context passed to this callback contains additional field `:incoming_demand`. 33 | """ 34 | @callback handle_demand( 35 | pad :: Pad.ref(), 36 | size :: non_neg_integer, 37 | unit :: Buffer.Metric.unit(), 38 | context :: CallbackContext.t(), 39 | state :: Element.state() 40 | ) :: 41 | {[ 42 | Membrane.Element.Action.common_actions() 43 | | Membrane.Element.Action.stream_actions() 44 | ], Element.state()} 45 | 46 | @optional_callbacks handle_demand: 5 47 | 48 | @doc PadsSpecs.def_pad_docs(:output, :element) 49 | defmacro def_output_pad(name, spec) do 50 | element_type = Module.get_attribute(__CALLER__.module, :__membrane_element_type__) 51 | PadsSpecs.def_pad(name, :output, spec, element_type) 52 | end 53 | 54 | defmacro __using__(_) do 55 | quote location: :keep do 56 | @behaviour unquote(__MODULE__) 57 | 58 | import unquote(__MODULE__), only: [def_output_pad: 2] 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/membrane/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Endpoint do 2 | @moduledoc """ 3 | Module defining behaviour for endpoints - elements consuming and producing data. 4 | 5 | Behaviours for endpoints are specified, besides this place, in modules 6 | `Membrane.Element.Base`, 7 | `Membrane.Element.WithOutputPads`, 8 | and `Membrane.Element.WithInputPads`. 9 | 10 | Endpoint can have both input and output pads. Job of usual endpoint is both, to 11 | receive some data on such pad and consume it (write to a soundcard, send through 12 | TCP etc.) and to produce some data (read from soundcard, download through HTTP, 13 | etc.) and send it through such pad. If the pad has the flow control set to 14 | `:manual`, then endpoint is also responsible for receiving demands on the output 15 | pad and requesting them on the input pad (for more details, see 16 | `c:Membrane.Element.WithOutputPads.handle_demand/5` callback). 17 | Endpoints, like all elements, can of course have multiple pads if needed to 18 | provide more complex solutions. 19 | """ 20 | 21 | alias Membrane.Core.DocsHelper 22 | 23 | @doc """ 24 | Brings all the stuff necessary to implement a endpoint element. 25 | 26 | Options: 27 | - `:bring_pad?` - if true (default) requires and aliases `Membrane.Pad` 28 | - `:flow_control_hints?` - if true (default) generates compile-time warnings \ 29 | if the number, direction, and type of flow control of pads are likely to cause unintended \ 30 | behaviours. 31 | """ 32 | defmacro __using__(options) do 33 | Module.put_attribute(__CALLER__.module, :__membrane_element_type__, :endpoint) 34 | 35 | quote location: :keep do 36 | use Membrane.Element.Base, unquote(options) 37 | use Membrane.Element.WithOutputPads 38 | use Membrane.Element.WithInputPads 39 | 40 | @doc false 41 | @spec membrane_element_type() :: Membrane.Element.type() 42 | def membrane_element_type, do: :endpoint 43 | end 44 | end 45 | 46 | DocsHelper.add_callbacks_list_to_moduledoc( 47 | __MODULE__, 48 | [Membrane.Element.Base, Membrane.Element.WithInputPads, Membrane.Element.WithOutputPads] 49 | ) 50 | end 51 | -------------------------------------------------------------------------------- /lib/membrane/event.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Event do 2 | @moduledoc """ 3 | Represents a communication event, capable of flowing both downstream and upstream. 4 | 5 | Events are dispatched using `t:Membrane.Element.Action.event/0` and are handled via the 6 | `c:Membrane.Element.Base.handle_event/4` callback. Each event must conform to the 7 | `Membrane.EventProtocol` to ensure the proper configuration of its behaviour. 8 | """ 9 | 10 | alias Membrane.EventProtocol 11 | 12 | @typedoc """ 13 | The Membrane event, based on the `Membrane.EventProtocol`. 14 | """ 15 | @type t :: EventProtocol.t() 16 | 17 | @doc """ 18 | Checks if the given argument is a Membrane event. 19 | 20 | Returns `true` if the `event` implements the `Membrane.EventProtocol`, otherwise `false`. 21 | """ 22 | @spec event?(t()) :: boolean 23 | def event?(event) do 24 | EventProtocol.impl_for(event) != nil 25 | end 26 | 27 | defdelegate sticky?(event), to: EventProtocol 28 | 29 | defdelegate async?(event), to: EventProtocol 30 | end 31 | -------------------------------------------------------------------------------- /lib/membrane/event/discontinuity.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Event.Discontinuity do 2 | @moduledoc """ 3 | Generic discontinuity event. 4 | 5 | This event means that flow of buffers in the stream was interrupted, but stream 6 | itself is not done. 7 | 8 | Frequent reasons for this are soundcards drops while capturing sound, network 9 | data loss etc. 10 | 11 | If duration of the discontinuity is known, it can be passed as an argument. 12 | """ 13 | @derive Membrane.EventProtocol 14 | 15 | @type duration :: Membrane.Time.t() | nil 16 | 17 | defstruct duration: nil 18 | 19 | @typedoc @moduledoc 20 | @type t :: %__MODULE__{duration: duration} 21 | end 22 | -------------------------------------------------------------------------------- /lib/membrane/event/underrun.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Event.Underrun do 2 | @moduledoc """ 3 | Generic underrun event. 4 | 5 | This event means that certain element is willing to consume more buffers, 6 | but there are none available. 7 | 8 | It makes sense to use this event as an upstream event to notify previous 9 | elements in the pipeline that they should generate more buffers. 10 | """ 11 | @derive Membrane.EventProtocol 12 | defstruct [] 13 | 14 | @typedoc @moduledoc 15 | @type t :: %__MODULE__{} 16 | end 17 | -------------------------------------------------------------------------------- /lib/membrane/event_protocol.ex: -------------------------------------------------------------------------------- 1 | defprotocol Membrane.EventProtocol do 2 | @moduledoc """ 3 | Protocol that allows to configure behaviour of `Membrane.Event`s. 4 | 5 | Each event has to implement or derive this protocol. 6 | """ 7 | 8 | @typedoc """ 9 | A type describing all the types that implement the `Membrane.EventProtocol` protocol. 10 | """ 11 | @type t :: struct 12 | 13 | @doc """ 14 | Specifies whether event is sent right away (not sticky), or it is 'pushed' by 15 | the next sent buffer (sticky). Defaults to false (not sticky). 16 | 17 | Returning a sticky event from a callback stores it in a queue. When the next 18 | buffer is to be sent, all events from the queue are sent before it. An example 19 | can be the `Membrane.Event.StartOfStream` event. 20 | """ 21 | @spec sticky?(t) :: boolean 22 | def sticky?(_event) 23 | 24 | @doc """ 25 | Determines whether event is synchronized with buffers (sync) or not (async). 26 | Defaults to false (sync). 27 | 28 | Buffers and sync events are always received in the same order they are 29 | sent. Async events are handled before any buffers enqueued that are waiting 30 | in Membrane internal queues to be processed. 31 | """ 32 | @spec async?(t) :: boolean 33 | def async?(_event) 34 | end 35 | 36 | defmodule Membrane.EventProtocol.DefaultImpl do 37 | @moduledoc """ 38 | Default implementation of `Membrane.EventProtocol`. 39 | 40 | If `use`d in `defimpl`, not implemented callbacks fallback to default ones. 41 | """ 42 | defmacro __using__(_args) do 43 | quote do 44 | @impl true 45 | def sticky?(_event), do: false 46 | 47 | @impl true 48 | def async?(_event), do: false 49 | 50 | defoverridable async?: 1, sticky?: 1 51 | end 52 | end 53 | end 54 | 55 | defimpl Membrane.EventProtocol, for: Any do 56 | use Membrane.EventProtocol.DefaultImpl 57 | end 58 | -------------------------------------------------------------------------------- /lib/membrane/fake_sink.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Fake.Sink do 2 | @moduledoc """ 3 | Membrane Sink that ignores incoming data. 4 | """ 5 | 6 | use Membrane.Sink 7 | 8 | def_input_pad :input, accepted_format: _any 9 | 10 | @impl true 11 | def handle_buffer(:input, _buffer, _ctx, state), do: {[], state} 12 | end 13 | -------------------------------------------------------------------------------- /lib/membrane/filter.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Filter do 2 | @moduledoc """ 3 | Module defining behaviour for filters - elements processing data. 4 | 5 | Behaviours for filters are specified, besides this place, in modules 6 | `Membrane.Element.Base`, 7 | `Membrane.Element.WithOutputPads`, 8 | and `Membrane.Element.WithInputPads`. 9 | 10 | Filters can have both input and output pads. Job of a usual filter is to 11 | receive some data on a input pad, process the data and send it through the 12 | output pad. If the pad has the flow control set to `:manual`, then filter 13 | is also responsible for receiving demands on the output pad and requesting 14 | them on the input pad (for more details, see 15 | `c:Membrane.Element.WithOutputPads.handle_demand/5` callback). 16 | Filters, like all elements, can of course have multiple pads if needed to 17 | provide more complex solutions. 18 | """ 19 | 20 | alias Membrane.Core.DocsHelper 21 | 22 | @doc """ 23 | Brings all the stuff necessary to implement a filter element. 24 | 25 | Options: 26 | - `:bring_pad?` - if true (default) requires and aliases `Membrane.Pad` 27 | - `:flow_control_hints?` - if true (default) generates compile-time warnings \ 28 | if the number, direction, and type of flow control of pads are likely to cause unintended \ 29 | behaviours. 30 | """ 31 | 32 | defmacro __using__(options) do 33 | Module.put_attribute(__CALLER__.module, :__membrane_element_type__, :filter) 34 | 35 | quote location: :keep do 36 | use Membrane.Element.Base, unquote(options) 37 | use Membrane.Element.WithOutputPads 38 | use Membrane.Element.WithInputPads 39 | 40 | @doc false 41 | @spec membrane_element_type() :: Membrane.Element.type() 42 | def membrane_element_type, do: :filter 43 | 44 | @impl true 45 | def handle_stream_format(_pad, stream_format, _context, state), 46 | do: {[forward: stream_format], state} 47 | 48 | @impl true 49 | def handle_event(_pad, event, _context, state), do: {[forward: event], state} 50 | 51 | @impl true 52 | def handle_end_of_stream(pad, _context, state), 53 | do: {[forward: :end_of_stream], state} 54 | 55 | defoverridable handle_stream_format: 4, 56 | handle_event: 4, 57 | handle_end_of_stream: 3 58 | end 59 | end 60 | 61 | DocsHelper.add_callbacks_list_to_moduledoc( 62 | __MODULE__, 63 | [Membrane.Element.Base, Membrane.Element.WithInputPads, Membrane.Element.WithOutputPads] 64 | ) 65 | end 66 | -------------------------------------------------------------------------------- /lib/membrane/funnel.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Funnel do 2 | @moduledoc """ 3 | Element that can be used for collecting data from multiple inputs and sending it through one 4 | output. 5 | 6 | When a new input connects in the `:playing` state, the funnel sends 7 | `#{inspect(__MODULE__)}.NewInputEvent` via output. 8 | """ 9 | 10 | use Membrane.Filter 11 | 12 | def_input_pad :input, accepted_format: _any, flow_control: :auto, availability: :on_request 13 | def_output_pad :output, accepted_format: _any, flow_control: :auto 14 | 15 | def_options end_of_stream: [spec: :on_last_pad | :on_first_pad | :never, default: :on_last_pad] 16 | 17 | @impl true 18 | def handle_init(_ctx, opts) do 19 | {[], %{end_of_stream: opts.end_of_stream}} 20 | end 21 | 22 | @impl true 23 | def handle_buffer(Pad.ref(:input, _id), buffer, _ctx, state) do 24 | {[buffer: {:output, buffer}], state} 25 | end 26 | 27 | @impl true 28 | def handle_pad_added(Pad.ref(:input, _id), %{playback_state: :playing}, state) do 29 | {[event: {:output, %__MODULE__.NewInputEvent{}}], state} 30 | end 31 | 32 | @impl true 33 | def handle_pad_added(Pad.ref(:input, _id), _ctx, state) do 34 | {[], state} 35 | end 36 | 37 | @impl true 38 | def handle_end_of_stream(Pad.ref(:input, _id), _ctx, %{end_of_stream: :never} = state) do 39 | {[], state} 40 | end 41 | 42 | @impl true 43 | def handle_end_of_stream(Pad.ref(:input, _id), ctx, %{end_of_stream: :on_first_pad} = state) do 44 | if ctx.pads.output.end_of_stream? do 45 | {[], state} 46 | else 47 | {[end_of_stream: :output], state} 48 | end 49 | end 50 | 51 | @impl true 52 | def handle_end_of_stream(Pad.ref(:input, _id), ctx, %{end_of_stream: :on_last_pad} = state) do 53 | if ctx |> inputs_data() |> Enum.all?(& &1.end_of_stream?) do 54 | {[end_of_stream: :output], state} 55 | else 56 | {[], state} 57 | end 58 | end 59 | 60 | defp inputs_data(ctx) do 61 | Enum.flat_map(ctx.pads, fn 62 | {Pad.ref(:input, _id), data} -> [data] 63 | _output -> [] 64 | end) 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/membrane/funnel/new_input_event.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Funnel.NewInputEvent do 2 | @moduledoc """ 3 | Event sent each time new element is linked (via funnel input pad) after playing pipeline. 4 | """ 5 | @derive Membrane.EventProtocol 6 | 7 | @type t :: %__MODULE__{} 8 | defstruct [] 9 | end 10 | -------------------------------------------------------------------------------- /lib/membrane/keyframe_request_event.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.KeyframeRequestEvent do 2 | @moduledoc """ 3 | Generic event for requesting a key frame. 4 | 5 | The key frame is meant as a part of stream such that 6 | the stream can be decoded from the beginning of each key 7 | frame without knowledge of the stream content before that 8 | point. 9 | """ 10 | @derive Membrane.EventProtocol 11 | 12 | defstruct [] 13 | 14 | @typedoc @moduledoc 15 | @type t :: %__MODULE__{} 16 | end 17 | -------------------------------------------------------------------------------- /lib/membrane/notification.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.ChildNotification do 2 | @moduledoc """ 3 | A child notification is a message sent from `Membrane.Element` or `Membrane.Bin` to a parent 4 | via action `t:Membrane.Element.Action.notify_parent` or `t:Membrane.Bin.Action.notify_parent` 5 | returned from any callback. 6 | 7 | A notification can be handled in parent with 8 | `c:Membrane.Parent.handle_child_notification/4` callback. 9 | """ 10 | 11 | @typedoc @moduledoc 12 | @type t :: any 13 | end 14 | 15 | defmodule Membrane.ParentNotification do 16 | @moduledoc """ 17 | A parent notification is a message sent from `Membrane.Parent` or `Membrane.Bin` to a child 18 | via action `t:Membrane.Pipeline.Action.notify_parent` or `t:Membrane.Bin.Action.notify_child` 19 | returned from any callback. 20 | 21 | A notification can be handled in child with `c:Membrane.Element.Base.handle_parent_notification/3` or 22 | `c:Membrane.Bin.handle_parent_notification/3` callback. 23 | """ 24 | @typedoc @moduledoc 25 | @type t :: any 26 | end 27 | -------------------------------------------------------------------------------- /lib/membrane/pipeline/callback_context.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Pipeline.CallbackContext do 2 | @moduledoc """ 3 | Module describing context passed to the `Membrane.Pipeline` callbacks. 4 | """ 5 | 6 | @typedoc """ 7 | Type describing context passed to the `Membrane.Pipeline` callbacks. 8 | 9 | Field `:from` is present only in `c:Membrane.Pipeline.handle_call/3`. 10 | 11 | Field `:start_of_stream_received?` is present only in 12 | `c:Membrane.Pipeline.handle_element_end_of_stream/4`. 13 | 14 | Field `:crash_initiator` is only present in `c:Membrane.Pipeline.handle_child_terminated/3` 15 | and `c:Membrane.Pipeline.handle_crash_group_down/3`. 16 | 17 | Fields `:members` and `:crash_reason` are present only in 18 | `c:Membrane.Pipeline.handle_crash_group_down/3`. 19 | 20 | Fields `:exit_reason` and `:group_name` are present only in 21 | `c:Membrane.Pipeline.handle_child_terminated/3`. 22 | """ 23 | @type t :: %{ 24 | :children => %{Membrane.Child.name() => Membrane.ChildEntry.t()}, 25 | :clock => Membrane.Clock.t(), 26 | :module => module(), 27 | :playback => Membrane.Playback.t(), 28 | :resource_guard => Membrane.ResourceGuard.t(), 29 | :utility_supervisor => Membrane.UtilitySupervisor.t(), 30 | optional(:from) => [GenServer.from()], 31 | optional(:members) => [Membrane.Child.name()], 32 | optional(:crash_initiator) => Membrane.Child.name() | nil, 33 | optional(:crash_reason) => :normal | :shutdown | {:shutdown, term()} | term(), 34 | optional(:start_of_stream_received?) => boolean(), 35 | optional(:exit_reason) => :normal | :shutdown | {:shutdown, term()} | term(), 36 | optional(:group_name) => Membrane.Child.group() | nil 37 | } 38 | end 39 | -------------------------------------------------------------------------------- /lib/membrane/playback.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Playback do 2 | @moduledoc """ 3 | Playback defines whether media is processed within a pipeline (`:playing`) or not (`:stopped`). 4 | 5 | Component playback will not enter `:playing` state until its parent playback is playing and 6 | all components from the `spec`, that started this component have ended their setups and linked 7 | their pads. 8 | 9 | By default, component setup ends with the end of `handle_setup/2` callback. 10 | If `{:setup, :incomplete}` is returned there, setup lasts until `{:setup, :complete}` 11 | is returned from antoher callback. 12 | 13 | Untils the setup lasts, the component won't enter `:playing` playback. 14 | """ 15 | 16 | @typedoc @moduledoc 17 | @type t :: :stopped | :playing 18 | end 19 | -------------------------------------------------------------------------------- /lib/membrane/rc_message.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.RCMessage do 2 | @moduledoc """ 3 | An abstract module aggregating all the messages that can be sent by the `RCPipeline`. 4 | 5 | Check `t:t/0` for available messages. 6 | """ 7 | 8 | @typedoc """ 9 | The type describing all possible `Membrane.RCMessage`s. 10 | """ 11 | @type t :: 12 | __MODULE__.Playing.t() 13 | | __MODULE__.StartOfStream.t() 14 | | __MODULE__.EndOfStream.t() 15 | | __MODULE__.Notification.t() 16 | | __MODULE__.Terminated.t() 17 | 18 | defmodule Playing do 19 | @moduledoc """ 20 | Message sent when the pipeline starts playing 21 | """ 22 | 23 | @typedoc @moduledoc 24 | @type t :: %__MODULE__{from: pid()} 25 | 26 | @enforce_keys [:from] 27 | defstruct @enforce_keys 28 | end 29 | 30 | defmodule StartOfStream do 31 | @moduledoc """ 32 | Message sent when some element of the pipeline receives the start of stream event on some pad. 33 | """ 34 | 35 | @typedoc @moduledoc 36 | @type t :: %__MODULE__{ 37 | from: pid(), 38 | element: Membrane.Element.name(), 39 | pad: Membrane.Pad.name() 40 | } 41 | 42 | @enforce_keys [:from, :element, :pad] 43 | defstruct @enforce_keys 44 | end 45 | 46 | defmodule EndOfStream do 47 | @moduledoc """ 48 | Message sent when some element of the pipeline receives the start of stream event on some pad. 49 | """ 50 | 51 | @typedoc @moduledoc 52 | @type t :: %__MODULE__{ 53 | from: pid(), 54 | element: Membrane.Element.name(), 55 | pad: Membrane.Pad.name() 56 | } 57 | 58 | @enforce_keys [:from, :element, :pad] 59 | defstruct @enforce_keys 60 | end 61 | 62 | defmodule Notification do 63 | @moduledoc """ 64 | Message sent when the some element of the pipeline receives a notification. 65 | """ 66 | 67 | @typedoc @moduledoc 68 | @type t :: %__MODULE__{ 69 | from: pid(), 70 | element: Membrane.Element.name(), 71 | data: Membrane.ParentNotification.t() 72 | } 73 | 74 | @enforce_keys [:from, :element, :data] 75 | defstruct @enforce_keys 76 | end 77 | 78 | defmodule Terminated do 79 | @moduledoc """ 80 | Message sent when the pipeline gracefully terminates. 81 | """ 82 | 83 | @typedoc @moduledoc 84 | @type t :: %__MODULE__{from: pid()} 85 | 86 | @enforce_keys [:from] 87 | defstruct @enforce_keys 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /lib/membrane/remote_stream.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.RemoteStream do 2 | @moduledoc """ 3 | Format describing an unparsed data stream. It should be used whenever outputting 4 | or accepting an unknown stream (not to be confused with _any_ stream, which 5 | can have well-specified format either), or a stream whose format can't/shouldn't 6 | be created at that stage. 7 | 8 | Parameters: 9 | - `:content_format` - format that is supposed to be carried in the stream, 10 | `nil` if unknown (default) 11 | - `:type` - either `:bytestream` (continuous stream) or `:packetized` (each buffer 12 | contains exactly one specified unit of data) 13 | """ 14 | 15 | @typedoc @moduledoc 16 | @type t :: %__MODULE__{ 17 | content_format: module | nil, 18 | type: :bytestream | :packetized 19 | } 20 | defstruct content_format: nil, type: :bytestream 21 | end 22 | -------------------------------------------------------------------------------- /lib/membrane/sink.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Sink do 2 | @moduledoc """ 3 | Module defining behaviour for sinks - elements consuming data. 4 | 5 | Behaviours for sinks are specified, besides this place, in modules 6 | `Membrane.Element.Base`, 7 | and `Membrane.Element.WithInputPads`. 8 | 9 | Sink elements can define only input pads. Job of a usual sink is to receive some 10 | data on such pad and consume it (write to a soundcard, send through TCP etc.). 11 | If the pad has the flow control set to `:manual`, then element is also responsible 12 | for requesting demands when it is able and willing to consume data (for more details, 13 | see `t:Membrane.Element.Action.demand/0`). Sinks, like all elements, can of course 14 | have multiple pads if needed to provide more complex solutions. 15 | """ 16 | 17 | alias Membrane.Core.DocsHelper 18 | 19 | @doc """ 20 | Brings all the stuff necessary to implement a sink element. 21 | 22 | Options: 23 | - `:bring_pad?` - if true (default) requires and aliases `Membrane.Pad` 24 | - `:flow_control_hints?` - if true (default) generates compile-time warnings \ 25 | if the number, direction, and type of flow control of pads are likely to cause unintended \ 26 | behaviours. 27 | """ 28 | defmacro __using__(options) do 29 | Module.put_attribute(__CALLER__.module, :__membrane_element_type__, :sink) 30 | 31 | quote location: :keep do 32 | use Membrane.Element.Base, unquote(options) 33 | use Membrane.Element.WithInputPads 34 | 35 | @doc false 36 | @spec membrane_element_type() :: Membrane.Element.type() 37 | def membrane_element_type, do: :sink 38 | end 39 | end 40 | 41 | DocsHelper.add_callbacks_list_to_moduledoc( 42 | __MODULE__, 43 | [Membrane.Element.Base, Membrane.Element.WithInputPads] 44 | ) 45 | end 46 | -------------------------------------------------------------------------------- /lib/membrane/source.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Source do 2 | @moduledoc """ 3 | Module that should be used in sources - elements producing data. Declares 4 | appropriate behaviours implementation and provides default callbacks implementation. 5 | 6 | Behaviours for sources are specified in modules 7 | `Membrane.Element.Base` and 8 | `Membrane.Element.WithOutputPads`. 9 | 10 | Source elements can define only output pads. Job of a usual source is to produce 11 | some data (read from soundcard, download through HTTP, etc.) and send it through 12 | such pad. If the pad has the flow control set to `:manual`, then element is also 13 | responsible for receiving demands and send buffers only if they have previously 14 | been demanded (for more details, see `c:Membrane.Element.WithOutputPads.handle_demand/5` 15 | callback). Sources, like all elements, can of course have multiple pads if needed to 16 | provide more complex solutions. 17 | """ 18 | 19 | @doc """ 20 | Brings all the stuff necessary to implement a source element. 21 | 22 | Options: 23 | - `:bring_pad?` - if true (default) requires and aliases `Membrane.Pad` 24 | - `:flow_control_hints?` - if true (default) generates compile-time warnings \ 25 | if the number, direction, and type of flow control of pads are likely to cause unintended \ 26 | behaviours. 27 | """ 28 | alias Membrane.Core.DocsHelper 29 | 30 | defmacro __using__(options) do 31 | Module.put_attribute(__CALLER__.module, :__membrane_element_type__, :source) 32 | 33 | quote location: :keep do 34 | use Membrane.Element.Base, unquote(options) 35 | use Membrane.Element.WithOutputPads 36 | 37 | @doc false 38 | @spec membrane_element_type() :: Membrane.Element.type() 39 | def membrane_element_type, do: :source 40 | end 41 | end 42 | 43 | DocsHelper.add_callbacks_list_to_moduledoc( 44 | __MODULE__, 45 | [Membrane.Element.Base, Membrane.Element.WithOutputPads] 46 | ) 47 | end 48 | -------------------------------------------------------------------------------- /lib/membrane/stream_format.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.StreamFormat do 2 | @moduledoc """ 3 | Defines the capabilities of a pad within the Membrane framework. 4 | 5 | Each pad in a multimedia pipeline has specific capabilities, determining the type and format 6 | of data it can handle. For example, a pad's capabilities might include handling raw audio 7 | with a specific sample rate or managing encoded audio in a specified format. 8 | 9 | To successfully link two pads together, their capabilities must be compatible. 10 | """ 11 | 12 | @typedoc """ 13 | Represents a pad's capabilities. For more information, see: `Membrane.StreamFormat`. 14 | """ 15 | @type t :: struct 16 | end 17 | -------------------------------------------------------------------------------- /lib/membrane/tee.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Tee do 2 | @moduledoc """ 3 | Element for forwarding buffers to at least one output pad 4 | 5 | It has one input pad `:input` and 2 output pads: 6 | * `:output` - is a dynamic pad which is always available and works in pull mode 7 | * `:push_output` - is a dynamic pad that can be linked to any number of elements (including 0) and works 8 | in push mode 9 | 10 | The `:output` pads dictate the speed of processing data and any element (or elements) connected to 11 | `:push_output` pad will receive the same data as all `:output` instances. 12 | """ 13 | use Membrane.Filter, flow_control_hints?: false 14 | 15 | require Membrane.Logger 16 | 17 | def_input_pad :input, 18 | availability: :always, 19 | flow_control: :auto, 20 | accepted_format: _any 21 | 22 | def_output_pad :output, 23 | availability: :on_request, 24 | flow_control: :auto, 25 | accepted_format: _any 26 | 27 | def_output_pad :push_output, 28 | availability: :on_request, 29 | flow_control: :push, 30 | accepted_format: _any 31 | 32 | @impl true 33 | def handle_init(_ctx, _opts) do 34 | {[], %{stream_format: nil}} 35 | end 36 | 37 | @impl true 38 | def handle_playing(ctx, state) do 39 | if map_size(ctx.pads) < 2 do 40 | Membrane.Logger.debug(""" 41 | #{inspect(__MODULE__)} enters :playing playback without any output (:output or :push_output) \ 42 | pads linked. 43 | """) 44 | end 45 | 46 | {[], state} 47 | end 48 | 49 | @impl true 50 | def handle_stream_format(:input, stream_format, _ctx, state) do 51 | {[forward: stream_format], %{state | stream_format: stream_format}} 52 | end 53 | 54 | @impl true 55 | def handle_pad_added(Pad.ref(name, _ref) = output_pad, ctx, state) 56 | when name in [:output, :push_output] do 57 | maybe_stream_format = 58 | case state.stream_format do 59 | nil -> [] 60 | stream_format -> [stream_format: {output_pad, stream_format}] 61 | end 62 | 63 | maybe_eos = 64 | if ctx.pads.input.end_of_stream?, 65 | do: [end_of_stream: output_pad], 66 | else: [] 67 | 68 | {maybe_stream_format ++ maybe_eos, state} 69 | end 70 | 71 | @impl true 72 | def handle_buffer(:input, buffer, _ctx, state) do 73 | {[forward: buffer], state} 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/membrane/testing/event.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Testing.Event do 2 | @moduledoc """ 3 | Empty event that can be used in tests 4 | """ 5 | 6 | @derive Membrane.EventProtocol 7 | defstruct [] 8 | end 9 | -------------------------------------------------------------------------------- /lib/membrane/testing/mock_resource_guard.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Testing.MockResourceGuard do 2 | @moduledoc """ 3 | Mock for `Membrane.ResourceGuard`. 4 | 5 | Informs the test process about registered cleanup functions and tags. 6 | Works with `Membrane.Testing.Assertions`, for example 7 | `Membrane.Testing.Assertions.assert_resource_guard_register/4`: 8 | 9 | iex> guard = #{inspect(__MODULE__)}.start_link_supervised!() 10 | ...> Membrane.ResourceGuard.register(guard, fn -> :abc end, tag: :some_tag) 11 | ...> import Membrane.Testing.Assertions 12 | ...> assert_resource_guard_register(guard, function, :some_tag) 13 | ...> function.() 14 | :abc 15 | 16 | """ 17 | use GenServer 18 | 19 | require Membrane.Core.Message, as: Message 20 | require Membrane.Core.Utils, as: Utils 21 | 22 | @type options :: [test_process: pid] 23 | 24 | @spec child_spec(test_process: pid()) :: Supervisor.child_spec() 25 | def child_spec(options) do 26 | super(options) |> Map.merge(%{restart: :transient, id: {__MODULE__, make_ref()}}) 27 | end 28 | 29 | @spec start_link(options) :: {:ok, pid} 30 | def start_link(options \\ []) do 31 | options = Keyword.put_new(options, :test_process, self()) 32 | GenServer.start_link(__MODULE__, options) 33 | end 34 | 35 | @spec start_link_supervised!(options) :: pid 36 | def start_link_supervised!(options \\ []) do 37 | options = Keyword.put_new(options, :test_process, self()) 38 | {:ok, pid} = ex_unit_start_supervised({__MODULE__, options}) 39 | pid 40 | end 41 | 42 | @impl true 43 | def init(options) do 44 | Utils.log_on_error do 45 | {:ok, Map.new(options)} 46 | end 47 | end 48 | 49 | @impl true 50 | def handle_info(msg, state) do 51 | Utils.log_on_error do 52 | do_handle_info(msg, state) 53 | end 54 | end 55 | 56 | defp do_handle_info(Message.new(:register, [function, options]), state) do 57 | tag = Keyword.fetch!(options, :tag) 58 | send_to_test_process(state, :register, {function, tag}) 59 | {:noreply, state} 60 | end 61 | 62 | defp do_handle_info(Message.new(:unregister, tag), state) do 63 | send_to_test_process(state, :unregister, tag) 64 | {:noreply, state} 65 | end 66 | 67 | defp do_handle_info(Message.new(:cleanup, tag), state) do 68 | send_to_test_process(state, :cleanup, tag) 69 | {:noreply, state} 70 | end 71 | 72 | defp ex_unit_start_supervised(child_spec) do 73 | # It's not a 'normal' call to keep dialyzer quiet 74 | apply(ExUnit.Callbacks, :start_supervised, [child_spec]) 75 | end 76 | 77 | defp send_to_test_process(%{test_process: test_process}, type, args) do 78 | send(test_process, {__MODULE__, self(), {type, args}}) 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /lib/membrane/testing/notification.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Testing.Notification do 2 | @moduledoc false 3 | 4 | # Notification sent internally by `Membrane.Testing.Pipeline` 5 | @enforce_keys [:payload] 6 | defstruct @enforce_keys 7 | end 8 | -------------------------------------------------------------------------------- /lib/membrane/utility_supervisor.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.UtilitySupervisor do 2 | @moduledoc """ 3 | A supervisor responsible for managing utility processes under the pipeline's 4 | supervision tree. 5 | 6 | The supervisor is spawned with each component and can be accessed from callback contexts. 7 | 8 | `Membrane.UtilitySupervisor` does not restart processes. Rather, it ensures that these utility processes 9 | terminate gracefully when the component that initiated them terminates. 10 | 11 | If a process needs to be able to restart, spawn a dedicated supervisor under this supervisor. 12 | 13 | ## Example 14 | 15 | def handle_setup(ctx, state) do 16 | Membrane.UtilitySupervisor.start_link_child( 17 | ctx.utility_supervisor, 18 | {MySupervisor, children: [SomeWorker, OtherWorker], restart: :one_for_one}) 19 | end 20 | 21 | """ 22 | 23 | @typedoc """ 24 | The pid of the `Membrane.UtilitySupervisor` process. 25 | """ 26 | @type t :: pid() 27 | 28 | @doc """ 29 | Starts a process under the utility supervisor. 30 | 31 | Semantics of the `child_spec` argument are the same as in `Supervisor.child_spec/2`. 32 | """ 33 | @spec start_child(t, Supervisor.child_spec() | {module(), term()} | module()) :: 34 | Supervisor.on_start_child() 35 | defdelegate start_child(supervisor, child_spec), 36 | to: Membrane.Core.SubprocessSupervisor, 37 | as: :start_utility 38 | 39 | @doc """ 40 | Starts a process under the utility supervisor and links it to the current process. 41 | 42 | Semantics of the `child_spec` argument are the same as in `Supervisor.child_spec/2`. 43 | """ 44 | @spec start_link_child(t, Supervisor.child_spec() | {module(), term()} | module()) :: 45 | Supervisor.on_start_child() 46 | defdelegate start_link_child(supervisor, child_spec), 47 | to: Membrane.Core.SubprocessSupervisor, 48 | as: :start_link_utility 49 | end 50 | -------------------------------------------------------------------------------- /priv/plts/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/membraneframework/membrane_core/e40054cffc90a25b5a7b141d47d24e42a728eb15/priv/plts/.gitkeep -------------------------------------------------------------------------------- /scripts/python/get_author_origin.py: -------------------------------------------------------------------------------- 1 | import sys, json; 2 | 3 | membrane_team = json.load(sys.stdin) 4 | pr_author = sys.argv[1] 5 | 6 | if pr_author == "membraneframeworkadmin": 7 | print("MEMBRANE") 8 | sys.exit(0) 9 | 10 | try: 11 | for person in membrane_team: 12 | if person["login"] == pr_author: 13 | print("MEMBRANE") 14 | sys.exit(0) 15 | 16 | print("COMMUNITY") 17 | except: 18 | print("An exception occurred in get_author_origin.py, provided JSON: ", membrane_team) 19 | print("Provided PR_AUTHOR: ", pr_author) 20 | sys.exit(1) 21 | -------------------------------------------------------------------------------- /scripts/python/get_ticket_id.py: -------------------------------------------------------------------------------- 1 | import sys, json; 2 | 3 | full_json = json.load(sys.stdin) 4 | pr_url = sys.argv[1] 5 | 6 | project_items = full_json["items"] 7 | 8 | item_id = None 9 | for item in project_items: 10 | if "content" in item and "url" in item["content"]: 11 | if item["content"]["url"] == pr_url: 12 | item_id = item["id"] 13 | break 14 | 15 | if item_id == None: 16 | print("Error occurred in get_ticket.py: ID of ticket related to PR", pr_url, "not found in the provided JSON") 17 | print("Provided JSON:", full_json) 18 | sys.exit(1) 19 | else: 20 | print(item_id) 21 | -------------------------------------------------------------------------------- /test/membrane/buffer_metric/byte_size_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Buffer.Metric.ByteSizeTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Membrane.Buffer 5 | alias Membrane.Buffer.Metric.ByteSize 6 | 7 | @pay1 <<0, 1, 2, 3, 4, 5>> 8 | @pay2 <<6, 7, 8, 9, 10, 11>> 9 | @buf1 %Buffer{payload: @pay1} 10 | @buf2 %Buffer{payload: @pay2} 11 | @single_buffer [@buf1] 12 | @buffers [@buf1, @buf2] 13 | 14 | describe ".buffers_size/1" do 15 | test "should return size of all buffers" do 16 | size = ByteSize.buffers_size(@buffers) 17 | assert size == byte_size(@pay1) + byte_size(@pay2) 18 | end 19 | end 20 | 21 | describe ".split_buffers/2" do 22 | test "when split position matches size of first buffer, extract only first buffer" do 23 | {buf, rest} = ByteSize.split_buffers(@buffers, byte_size(@pay1)) 24 | assert buf == [@buf1] 25 | assert rest == [@buf2] 26 | end 27 | 28 | test "when there is only one buffer where split position is greater than buffer size \ 29 | returns the buffer and an empty list" do 30 | {buf, []} = ByteSize.split_buffers(@single_buffer, byte_size(@pay1) + 10) 31 | assert buf == [@buf1] 32 | end 33 | 34 | test "when there is only one buffer where split position is 0, it returns an empty \ 35 | list and a list with the buffer" do 36 | {[], rest} = ByteSize.split_buffers(@single_buffer, 0) 37 | assert rest == [@buf1] 38 | end 39 | 40 | test "when splitting is necessary it extracts the first buffer and splits the second into two" do 41 | {extracted, rest} = ByteSize.split_buffers(@buffers, byte_size(@pay1) + 1) 42 | <> = @pay2 43 | assert extracted == [@buf1, %Membrane.Buffer{payload: one_byte}] 44 | assert rest == [%Membrane.Buffer{payload: expected_rest}] 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /test/membrane/buffer_metric/count_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Buffer.Metric.CountTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Membrane.Buffer 5 | alias Membrane.Buffer.Metric.Count 6 | 7 | @buf1 %Buffer{payload: :pay1} 8 | @buf2 %Buffer{payload: :pay2} 9 | @buffers [@buf1, @buf2] 10 | @count 1 11 | 12 | describe ".buffers_size/1" do 13 | test "should return count of all buffers" do 14 | assert Count.buffers_size(@buffers) == 2 15 | end 16 | end 17 | 18 | describe ".split_buffers/2" do 19 | test "should return split buffers" do 20 | {extracted, rest} = Count.split_buffers(@buffers, @count) 21 | 22 | assert extracted == [@buf1] 23 | assert rest == [@buf2] 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/membrane/buffer_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Membrane.BufferTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Membrane.Buffer 5 | 6 | describe "Buffer.get_dts_or_pts" do 7 | test "returns dts when both dts and pts are set" do 8 | buffer = %Buffer{payload: <<>>, dts: 1, pts: 0} 9 | assert Buffer.get_dts_or_pts(buffer) == 1 10 | end 11 | 12 | test "returns pts when only pts is set" do 13 | buffer = %Buffer{payload: <<>>, pts: 1} 14 | assert Buffer.get_dts_or_pts(buffer) == 1 15 | end 16 | 17 | test "returns dts when only dts is set" do 18 | buffer = %Buffer{payload: <<>>, dts: 1} 19 | assert Buffer.get_dts_or_pts(buffer) == 1 20 | end 21 | 22 | test "returns nil when both dts and pts are not set" do 23 | buffer = %Buffer{payload: <<>>} 24 | assert Buffer.get_dts_or_pts(buffer) == nil 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/membrane/core/element/lifecycle_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Core.Element.LifecycleControllerTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Membrane.Core.Element.{AtomicDemand, LifecycleController, State} 5 | alias Membrane.Core.Element.ManualFlowController.InputQueue 6 | 7 | alias Membrane.Core.{ 8 | Message, 9 | SubprocessSupervisor 10 | } 11 | 12 | require Membrane.Core.Message 13 | 14 | defmodule DummyElement do 15 | use Membrane.Filter 16 | def_output_pad :output, flow_control: :manual, accepted_format: _any 17 | 18 | @impl true 19 | def handle_terminate_request(_ctx, state) do 20 | {[], state} 21 | end 22 | end 23 | 24 | setup do 25 | atomic_demand = 26 | AtomicDemand.new(%{ 27 | receiver_effective_flow_control: :pull, 28 | receiver_process: self(), 29 | receiver_demand_unit: :buffers, 30 | sender_process: self(), 31 | sender_pad_ref: :some_pad, 32 | supervisor: SubprocessSupervisor.start_link!() 33 | }) 34 | 35 | input_queue = 36 | InputQueue.new(%{ 37 | inbound_demand_unit: :buffers, 38 | outbound_demand_unit: :buffers, 39 | atomic_demand: atomic_demand, 40 | pad_ref: :some_pad, 41 | log_tag: "test", 42 | target_size: nil 43 | }) 44 | 45 | state = 46 | struct!(State, 47 | module: DummyElement, 48 | name: :test_name, 49 | type: :filter, 50 | playback: :playing, 51 | parent_pid: self(), 52 | synchronization: %{clock: nil, parent_clock: nil}, 53 | delay_demands?: false, 54 | pads_to_snapshot: MapSet.new(), 55 | delayed_demands: MapSet.new(), 56 | pads_data: %{ 57 | input: 58 | struct(Membrane.Element.PadData, 59 | ref: :input, 60 | direction: :input, 61 | pid: self(), 62 | flow_control: :manual, 63 | start_of_stream?: true, 64 | end_of_stream?: false, 65 | input_queue: input_queue, 66 | demand: 0 67 | ) 68 | }, 69 | satisfied_auto_output_pads: MapSet.new(), 70 | awaiting_auto_input_pads: MapSet.new(), 71 | auto_input_pads: [] 72 | ) 73 | 74 | assert_received Message.new(:atomic_demand_increased, :some_pad) 75 | [state: state] 76 | end 77 | 78 | test "End of stream is generated upon termination", %{ 79 | state: state 80 | } do 81 | state = LifecycleController.handle_terminate_request(state) 82 | assert state.pads_data.input.end_of_stream? 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /test/membrane/core/helper/fast_map_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Core.Helper.FastMapTest do 2 | use ExUnit.Case, async: true 3 | 4 | require Membrane.Core.Helper.FastMap, as: FastMap 5 | 6 | doctest FastMap 7 | 8 | @map %{ 9 | list: [], 10 | counter: 0, 11 | pad: %{ 12 | :input => %{cnt: 1}, 13 | {:input, 0} => %{cnt: 1} 14 | }, 15 | and_some: :atom 16 | } 17 | 18 | test "get_in" do 19 | some_key = {:input, 0} 20 | assert 1 == FastMap.get_in!(@map, [:pad, some_key, :cnt]) 21 | 22 | assert_raise MatchError, fn -> 23 | unknown_key = {:input, Enum.random(2..3)} 24 | FastMap.get_in!(@map, [:pad, unknown_key, :cnt]) 25 | end 26 | end 27 | 28 | test "set_in" do 29 | some_key = {:input, 0} 30 | new_map = FastMap.set_in!(@map, [:pad, some_key, :cnt], 2) 31 | assert new_map.pad[some_key].cnt == 2 32 | 33 | assert_raise MatchError, fn -> 34 | unknown_key = {:input, Enum.random(2..3)} 35 | FastMap.set_in!(@map, [:pad, unknown_key, :cnt], 2) 36 | end 37 | 38 | assert_raise KeyError, fn -> 39 | unknown_key = {:input, Enum.random(2..3)} 40 | FastMap.set_in!(@map, [:pad, unknown_key], :something) 41 | end 42 | end 43 | 44 | test "update_in" do 45 | some_key = {:input, 0} 46 | new_map = FastMap.update_in!(@map, [:pad, some_key, :cnt], &(&1 + 1)) 47 | assert new_map.pad[some_key].cnt == 2 48 | 49 | assert_raise MatchError, fn -> 50 | unknown_key = {:input, Enum.random(2..3)} 51 | FastMap.update_in!(@map, [:pad, unknown_key, :cnt], &(&1 + 1)) 52 | end 53 | end 54 | 55 | test "get_and_update_in" do 56 | some_key = {:input, 0} 57 | assert {1, new_map} = FastMap.get_and_update_in!(@map, [:pad, some_key, :cnt], &{&1, &1 + 1}) 58 | assert new_map.pad[some_key].cnt == 2 59 | 60 | assert_raise MatchError, fn -> 61 | unknown_key = {:input, Enum.random(2..3)} 62 | FastMap.get_and_update_in!(@map, [:pad, unknown_key, :cnt], &{&1, &1 + 1}) 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /test/membrane/core/message_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Core.MessageTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Membrane.Core.Message 5 | 6 | require Membrane.Core.Message 7 | 8 | defmodule Receiver do 9 | use GenServer 10 | 11 | @impl GenServer 12 | def init(_opts), do: {:ok, nil} 13 | 14 | @impl GenServer 15 | def handle_call(Message.new(:request), _from, state), do: {:reply, :ok, state} 16 | end 17 | 18 | describe "call should" do 19 | test "return response when receiver process is alive" do 20 | {:ok, receiver} = GenServer.start_link(Receiver, []) 21 | 22 | response = Message.call(receiver, :request) 23 | assert response == :ok 24 | end 25 | 26 | test "return error when receiver process is not alive" do 27 | Process.flag(:trap_exit, true) 28 | pid = spawn_link(fn -> :ok end) 29 | assert_receive {:EXIT, ^pid, :normal} 30 | 31 | response = Message.call(pid, :request, [], [], 500) 32 | 33 | assert match?({:error, {:call_failure, _}}, response) 34 | end 35 | end 36 | 37 | describe "call! should" do 38 | test "return response when receiver process is alive" do 39 | {:ok, receiver} = GenServer.start_link(Receiver, []) 40 | 41 | response = Message.call!(receiver, :request) 42 | assert response == :ok 43 | end 44 | 45 | test "crash when receiver process is not alive" do 46 | Process.flag(:trap_exit, true) 47 | pid = spawn_link(fn -> :ok end) 48 | assert_receive {:EXIT, ^pid, :normal} 49 | 50 | caller_pid = 51 | spawn_link(fn -> 52 | Message.call!(pid, :request, [], [], 500) 53 | end) 54 | 55 | assert_receive {:EXIT, ^caller_pid, {:noproc, _details}} 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /test/membrane/filter_aggregator/integration_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Membrane.FilterAggregator.IntegrationTest do 2 | use ExUnit.Case, async: true 3 | 4 | import Membrane.ChildrenSpec 5 | import Membrane.Testing.Assertions 6 | 7 | alias Membrane.Buffer 8 | alias Membrane.FilterAggregator 9 | alias Membrane.RemoteStream 10 | alias Membrane.Testing.{Pipeline, Sink, Source} 11 | 12 | defmodule FilterA do 13 | use Membrane.Filter 14 | 15 | def_input_pad :input, flow_control: :auto, accepted_format: RemoteStream 16 | def_output_pad :output, flow_control: :auto, accepted_format: RemoteStream 17 | 18 | @impl true 19 | def handle_buffer(:input, %Buffer{payload: <>}, _ctx, state) do 20 | payload = for <>, into: <<>>, do: <> 21 | {[buffer: {:output, %Buffer{payload: <>}}], state} 22 | end 23 | end 24 | 25 | defmodule FilterB do 26 | use Membrane.Filter 27 | 28 | def_input_pad :input, flow_control: :auto, accepted_format: RemoteStream 29 | def_output_pad :output, flow_control: :auto, accepted_format: RemoteStream 30 | 31 | @impl true 32 | def handle_buffer(:input, %Buffer{payload: <>}, _ctx, state) do 33 | payload = for <>, into: <<>>, do: <> 34 | buffer = %Buffer{payload: <>} 35 | {[buffer: {:output, buffer}], state} 36 | end 37 | end 38 | 39 | test "pipeline with 2 filters" do 40 | payload = for i <- 3..256, into: <<>>, do: <> 41 | buffers_num_range = 1..100 42 | output = for i <- buffers_num_range, do: <> 43 | 44 | links = [ 45 | child(:src, %Source{output: output}) 46 | |> child(:filters, %FilterAggregator{ 47 | filters: [ 48 | a: FilterA, 49 | b: FilterB 50 | ] 51 | }) 52 | |> child(:sink, Sink) 53 | ] 54 | 55 | pid = Pipeline.start_link_supervised!(spec: links) 56 | assert_start_of_stream(pid, :sink) 57 | assert_sink_stream_format(pid, :sink, %RemoteStream{}) 58 | 59 | expected_payload = for i <- 0..253, into: <<>>, do: <> 60 | 61 | for i <- buffers_num_range do 62 | assert_sink_buffer(pid, :sink, %Membrane.Buffer{payload: <<^i, ^expected_payload::binary>>}) 63 | end 64 | 65 | assert_end_of_stream(pid, :sink) 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /test/membrane/filter_aggregator/internal_action_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Membrane.FilterAggregator.InternalActionTest do 2 | use ExUnit.Case, async: true 3 | 4 | require Membrane.Core.FilterAggregator.InternalAction, as: IA 5 | 6 | test "is_internal_action" do 7 | assert IA.is_internal_action(IA.setup()) 8 | assert IA.is_internal_action(IA.start_of_stream(:output)) 9 | end 10 | 11 | test "Using macros as patterns" do 12 | pad = :output 13 | assert IA.start_of_stream(_ignored) = IA.start_of_stream(pad) 14 | assert IA.start_of_stream(^pad) = IA.start_of_stream(:output) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test/membrane/integration/debug_elements_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Integration.DebugElementsTest do 2 | use ExUnit.Case, async: true 3 | 4 | import Membrane.ChildrenSpec 5 | import Membrane.Testing.Assertions 6 | 7 | alias Membrane.Buffer 8 | alias Membrane.Debug 9 | alias Membrane.Testing 10 | 11 | test "Membrane.Debug.Filter calls function passed in :handle_buffer and forwards buffers on :output pad" do 12 | payloads = Enum.map(1..100, &inspect/1) 13 | test_pid = self() 14 | 15 | spec = 16 | child(:source, %Testing.Source{output: payloads}) 17 | |> child(%Debug.Filter{handle_buffer: &send(test_pid, {:buffer, &1})}) 18 | |> child(:sink, Testing.Sink) 19 | 20 | pipeline = Testing.Pipeline.start_link_supervised!(spec: spec) 21 | 22 | assert_sink_stream_format(pipeline, :sink, _any) 23 | 24 | for expected_payload <- payloads do 25 | assert_sink_buffer(pipeline, :sink, %Buffer{payload: ^expected_payload}) 26 | assert_receive {:buffer, %Buffer{payload: ^expected_payload}} 27 | end 28 | 29 | Testing.Pipeline.terminate(pipeline) 30 | end 31 | 32 | test "Membrane.Debug.Sink calls function passed in :handle_buffer" do 33 | payloads = Enum.map(1..100, &inspect/1) 34 | test_pid = self() 35 | 36 | spec = 37 | child(:source, %Testing.Source{output: payloads}) 38 | |> child(:sink, %Debug.Sink{ 39 | handle_buffer: &send(test_pid, {:buffer, &1}), 40 | handle_stream_format: &send(test_pid, {:stream_format, &1}) 41 | }) 42 | 43 | pipeline = Testing.Pipeline.start_link_supervised!(spec: spec) 44 | 45 | assert_receive {:stream_format, _any} 46 | 47 | for expected_payload <- payloads do 48 | assert_receive {:buffer, %Buffer{payload: ^expected_payload}} 49 | end 50 | 51 | Testing.Pipeline.terminate(pipeline) 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /test/membrane/integration/distributed_pipeline_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Integration.DistributedPipelineTest do 2 | use ExUnit.Case, async: true 3 | 4 | import Membrane.Testing.Assertions 5 | 6 | alias Membrane.Support.Distributed 7 | alias Membrane.Testing 8 | 9 | setup do 10 | another_node = start_another_node() 11 | on_exit(fn -> kill_node(another_node) end) 12 | [first_node: node(self()), second_node: another_node] 13 | end 14 | 15 | test "if distributed pipeline works properly", context do 16 | pipeline = 17 | Testing.Pipeline.start_link_supervised!( 18 | module: Distributed.Pipeline, 19 | custom_args: context 20 | ) 21 | 22 | assert_pipeline_notified(pipeline, :sink_bin, :end_of_stream) 23 | 24 | assert context.first_node == node(pipeline) 25 | 26 | assert context.first_node == 27 | Testing.Pipeline.get_child_pid!(pipeline, :source) 28 | |> node() 29 | 30 | assert context.second_node == 31 | Testing.Pipeline.get_child_pid!(pipeline, :sink_bin) 32 | |> node() 33 | 34 | assert context.second_node == 35 | Testing.Pipeline.get_child_pid!(pipeline, [:sink_bin, :sink]) 36 | |> node() 37 | 38 | Testing.Pipeline.terminate(pipeline) 39 | end 40 | 41 | defp start_another_node() do 42 | {:ok, _pid, hostname} = :peer.start(%{host: ~c"127.0.0.1", name: :second}) 43 | :rpc.block_call(hostname, :code, :add_paths, [:code.get_path()]) 44 | hostname 45 | end 46 | 47 | defp kill_node(node) do 48 | :rpc.call(node, :init, :stop, []) 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /test/membrane/integration/end_of_stream_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Membrane.EndOfStreamTest do 2 | use ExUnit.Case, async: true 3 | 4 | import Membrane.ChildrenSpec 5 | import Membrane.Testing.Assertions 6 | 7 | alias Membrane.Testing 8 | 9 | defmodule EOSSource do 10 | use Membrane.Source 11 | 12 | def_output_pad :output, flow_control: :push, accepted_format: _any 13 | 14 | @impl true 15 | def handle_playing(_ctx, state) do 16 | {[end_of_stream: :output], state} 17 | end 18 | end 19 | 20 | defmodule EOSSink do 21 | use Membrane.Sink 22 | 23 | def_input_pad :input, flow_control: :push, accepted_format: _any 24 | 25 | @impl true 26 | def handle_start_of_stream(_pad, _ctx, _state) do 27 | raise "This callback shouldn't be invoked" 28 | end 29 | 30 | @impl true 31 | def handle_end_of_stream(:input, %{start_of_stream_received?: false}, state) do 32 | {[], state} 33 | end 34 | end 35 | 36 | test "send end of stream without start of stream" do 37 | pipeline = 38 | Testing.Pipeline.start_link_supervised!( 39 | spec: 40 | child(:source, EOSSource) 41 | |> child(:sink, EOSSink) 42 | ) 43 | 44 | assert_end_of_stream(pipeline, :sink) 45 | 46 | Testing.Pipeline.terminate(pipeline) 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /test/membrane/integration/endpoint_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Core.EndpointTest do 2 | use ExUnit.Case, async: true 3 | 4 | import Membrane.ChildrenSpec 5 | import Membrane.Testing.Assertions 6 | 7 | alias Membrane.Support.Bin.TestBins.TestFilter 8 | alias Membrane.Testing 9 | 10 | require Membrane.Core.Message 11 | 12 | describe "Starting and transmitting buffers" do 13 | test "with one endpoint and filter" do 14 | buffers = [~c"a", ~c"b", ~c"c"] 15 | 16 | pipeline = 17 | Testing.Pipeline.start_link_supervised!( 18 | spec: [ 19 | child(:endpoint, %Testing.Endpoint{output: buffers}) |> child(:filter, TestFilter), 20 | get_child(:filter) |> get_child(:endpoint) 21 | ] 22 | ) 23 | 24 | assert_data_flows_through(pipeline, buffers, :endpoint) 25 | end 26 | 27 | test "with one endpoint and many filters in between" do 28 | buffers = [~c"a", ~c"b", ~c"c"] 29 | 30 | pipeline = 31 | Testing.Pipeline.start_link_supervised!( 32 | spec: 33 | [ 34 | child(:endpoint, %Testing.Endpoint{output: buffers}), 35 | child(:filter1, TestFilter), 36 | child(:filter2, TestFilter), 37 | child(:filter3, TestFilter) 38 | ] ++ 39 | [ 40 | get_child(:endpoint) |> get_child(:filter1), 41 | get_child(:filter1) |> get_child(:filter2), 42 | get_child(:filter2) |> get_child(:filter3), 43 | get_child(:filter3) |> get_child(:endpoint) 44 | ] 45 | ) 46 | 47 | assert_data_flows_through(pipeline, buffers, :endpoint) 48 | end 49 | end 50 | 51 | defp assert_data_flows_through(pipeline, buffers, receiving_element) do 52 | assert_start_of_stream(pipeline, ^receiving_element) 53 | 54 | buffers 55 | |> Enum.each(fn b -> 56 | assert_sink_buffer(pipeline, receiving_element, %Membrane.Buffer{payload: ^b}) 57 | end) 58 | 59 | assert_end_of_stream(pipeline, ^receiving_element) 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /test/membrane/integration/funnel_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Integration.FunnelTest do 2 | use ExUnit.Case 3 | 4 | import Membrane.Testing.Assertions 5 | 6 | alias Membrane.{Buffer, Funnel, Testing} 7 | 8 | test "Collects multiple inputs" do 9 | import Membrane.ChildrenSpec 10 | data = 1..10 11 | 12 | {:ok, _supervisor_pid, pipeline} = 13 | Testing.Pipeline.start_link( 14 | spec: [ 15 | child(:funnel, Funnel), 16 | child(:src1, %Testing.Source{output: data}) |> get_child(:funnel), 17 | child(:src2, %Testing.Source{output: data}) |> get_child(:funnel), 18 | get_child(:funnel) |> child(:sink, Testing.Sink) 19 | ] 20 | ) 21 | 22 | data 23 | |> Enum.flat_map(&[&1, &1]) 24 | |> Enum.each(fn payload -> 25 | assert_sink_buffer(pipeline, :sink, %Buffer{payload: ^payload}) 26 | end) 27 | 28 | assert_end_of_stream(pipeline, :sink) 29 | refute_sink_buffer(pipeline, :sink, _buffer, 0) 30 | 31 | Membrane.Pipeline.terminate(pipeline) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/membrane/integration/no_stream_format_crash_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Membrane.FailWhenNoStreamFormatAreSent do 2 | use ExUnit.Case, async: true 3 | 4 | import Membrane.Testing.Assertions 5 | import Membrane.ChildrenSpec 6 | 7 | alias Membrane.Testing.{Pipeline, Sink, Source} 8 | 9 | defmodule SourceWhichDoesNotSendStreamFormat do 10 | use Membrane.Source 11 | 12 | def_output_pad :output, accepted_format: _any, flow_control: :manual 13 | 14 | @impl true 15 | def handle_init(_ctx, _options) do 16 | {[], %{}} 17 | end 18 | 19 | @impl true 20 | def handle_demand(_pad, _size, _unit, _ctx, state) do 21 | {[], state} 22 | end 23 | 24 | @impl true 25 | def handle_parent_notification(:send_buffer, _ctx, state) do 26 | {[buffer: {:output, %Membrane.Buffer{payload: "Something"}}], state} 27 | end 28 | 29 | @impl true 30 | def handle_parent_notification({:send_your_pid, requester_pid}, _ctx, state) do 31 | send(requester_pid, {:my_pid, self()}) 32 | {[], state} 33 | end 34 | end 35 | 36 | test "if pipeline crashes when the stream format are not sent before the first buffer" do 37 | links = [ 38 | child(:source, SourceWhichDoesNotSendStreamFormat) 39 | |> child(:sink, Sink) 40 | ] 41 | 42 | options = [ 43 | spec: links 44 | ] 45 | 46 | pipeline = Pipeline.start_supervised!(options) 47 | Pipeline.notify_child(pipeline, :source, {:send_your_pid, self()}) 48 | 49 | source_pid = 50 | receive do 51 | {:my_pid, pid} -> pid 52 | end 53 | 54 | source_ref = Process.monitor(source_pid) 55 | 56 | Pipeline.notify_child(pipeline, :source, :send_buffer) 57 | assert_receive {:DOWN, ^source_ref, :process, ^source_pid, {reason, _stack_trace}} 58 | assert %Membrane.ElementError{message: action_error_msg} = reason 59 | assert action_error_msg =~ ~r/buffer.*stream.*format.*not.*sent/ 60 | end 61 | 62 | test "if pipeline works properly when stream format are sent before the first buffer" do 63 | links = [ 64 | child(:source, Source) 65 | |> child(:sink, Sink) 66 | ] 67 | 68 | options = [ 69 | spec: links 70 | ] 71 | 72 | pipeline = Pipeline.start_supervised!(options) 73 | ref = Process.monitor(pipeline) 74 | assert_start_of_stream(pipeline, :sink) 75 | refute_receive {:DOWN, ^ref, :process, ^pipeline, _reason} 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /test/membrane/integration/sync_test/ticking_pace.exs: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Integration.SyncTest.TickingPace do 2 | use ExUnit.Case, async: false 3 | 4 | import Membrane.ChildrenSpec 5 | 6 | alias Membrane.Support.Sync 7 | alias Membrane.{Testing, Time} 8 | 9 | @tag :long_running 10 | test "Ratio modifies ticking pace correctly" do 11 | tick_interval = 100 12 | tries = 300 13 | ratio_error = 0.1 14 | 15 | actual_report_interval = 100 16 | reported_interval = 300 17 | 18 | spec = { 19 | child(:source, %Sync.Source{ 20 | tick_interval: tick_interval |> Time.milliseconds(), 21 | test_process: self() 22 | }) 23 | |> child(:sink, Sync.Sink), 24 | clock_provider: :sink 25 | } 26 | 27 | pipeline = Testing.Pipeline.start_link_supervised!(spec: spec) 28 | 29 | %{synchronization: %{clock_provider: %{clock: original_clock, provider: :sink}}} = 30 | :sys.get_state(pipeline) 31 | 32 | for _ <- 1..tries do 33 | send(original_clock, {:membrane_clock_update, reported_interval}) 34 | Process.sleep(actual_report_interval) 35 | end 36 | 37 | Testing.Pipeline.terminate(pipeline) 38 | 39 | ticks_amount = Sync.Helper.receive_ticks() 40 | 41 | actual_test_time = tries * actual_report_interval 42 | expected_ratio = 3.0 43 | actual_tick_time = actual_test_time / ticks_amount 44 | 45 | assert_in_delta tick_interval / actual_tick_time, expected_ratio, ratio_error 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /test/membrane/integration/synchronous_pipeline_call_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PipelineSynchronousCallTest do 2 | use ExUnit.Case, async: true 3 | 4 | import Membrane.Testing.Assertions 5 | 6 | alias Membrane.Pipeline 7 | 8 | @msg "Some message" 9 | defmodule TestPipeline do 10 | use Membrane.Pipeline 11 | @impl true 12 | def handle_init(_ctx, result) do 13 | result || {[], %{}} 14 | end 15 | 16 | @impl true 17 | def handle_child_notification(notification, child, _ctx, state) do 18 | {[], Map.put(state, :notification, {notification, child})} 19 | end 20 | 21 | @impl true 22 | def handle_info({{:please_reply, msg}, pid}, _ctx, state) do 23 | {[reply_to: {pid, msg}], state} 24 | end 25 | 26 | @impl true 27 | def handle_call({:instant_reply, msg}, _ctx, state) do 28 | {[reply: msg], state} 29 | end 30 | 31 | @impl true 32 | def handle_call({:postponed_reply, msg}, ctx, state) do 33 | send(self(), {{:please_reply, msg}, ctx.from}) 34 | {[], state} 35 | end 36 | end 37 | 38 | test "Pipeline should be able to reply to a call with :reply_to action" do 39 | pid = Membrane.Testing.Pipeline.start_link_supervised!(module: TestPipeline) 40 | 41 | reply = Pipeline.call(pid, {:postponed_reply, @msg}) 42 | assert reply == @msg 43 | 44 | Pipeline.terminate(pid) 45 | end 46 | 47 | test "Pipeline should be able to reply to a call with :reply action" do 48 | pid = Membrane.Testing.Pipeline.start_link_supervised!(module: TestPipeline) 49 | 50 | reply = Pipeline.call(pid, {:instant_reply, @msg}) 51 | assert reply == @msg 52 | 53 | Pipeline.terminate(pid) 54 | end 55 | 56 | defmodule PipelineSpawningChildrenOnCall do 57 | use Membrane.Pipeline 58 | 59 | @impl true 60 | def handle_init(_ctx, _options) do 61 | {[], %{}} 62 | end 63 | 64 | @impl true 65 | def handle_call(:spawn_children, _ctx, state) do 66 | spec = 67 | child(:source, %Membrane.Testing.Source{output: [1, 2, 3]}) 68 | |> child(:sink, Membrane.Testing.Sink) 69 | 70 | {[spec: spec, reply: nil], state} 71 | end 72 | end 73 | 74 | test "Pipeline should be able to perform actions before replying on handle_call" do 75 | {:ok, _supervisor, pipeline_pid} = 76 | Membrane.Testing.Pipeline.start(module: PipelineSpawningChildrenOnCall) 77 | 78 | Pipeline.call(pipeline_pid, :spawn_children) 79 | assert_end_of_stream(pipeline_pid, :sink) 80 | 81 | Pipeline.terminate(pipeline_pid) 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /test/membrane/integration/tee_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Integration.TeeTest do 2 | @moduledoc false 3 | use ExUnit.Case, async: true 4 | use Bunch 5 | 6 | import Membrane.ChildrenSpec 7 | import Membrane.Testing.Assertions 8 | 9 | alias Membrane.Buffer 10 | alias Membrane.Testing.{Pipeline, Sink, Source} 11 | 12 | test "forwards input to three all outputs" do 13 | range = 1..100 14 | sinks = [:sink1, :sink2, :sink3, :sink_4] 15 | 16 | spec = 17 | [ 18 | child(:src, %Source{output: range}) 19 | |> child(:tee, Membrane.Tee) 20 | ] ++ 21 | for sink <- sinks do 22 | pad = if sink in [:sink1, :sink2], do: :output, else: :push_output 23 | 24 | get_child(:tee) 25 | |> via_out(pad) 26 | |> child(sink, %Sink{}) 27 | end 28 | 29 | pipeline = Pipeline.start_link_supervised!(spec: spec) 30 | 31 | for sink <- sinks do 32 | assert_end_of_stream(pipeline, ^sink, :input) 33 | end 34 | 35 | for element <- range, sink <- sinks do 36 | assert_sink_buffer(pipeline, sink, %Buffer{payload: ^element}) 37 | end 38 | 39 | for {pad, sink} <- [push_output: :sink5, output: :sink6] do 40 | spec = 41 | get_child(:tee) 42 | |> via_out(pad) 43 | |> child(sink, %Sink{}) 44 | 45 | Pipeline.execute_actions(pipeline, spec: spec) 46 | end 47 | 48 | for sink <- [:sink5, :sink6] do 49 | assert_sink_stream_format(pipeline, sink, %Membrane.RemoteStream{}) 50 | assert_end_of_stream(pipeline, ^sink, :input) 51 | end 52 | 53 | Pipeline.terminate(pipeline) 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /test/membrane/integration/timer_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Integration.TimerTest do 2 | use ExUnit.Case, async: true 3 | 4 | import Membrane.ChildrenSpec 5 | import Membrane.Testing.Assertions 6 | 7 | alias Membrane.{Pipeline, Testing, Time} 8 | 9 | defmodule Element do 10 | use Membrane.Source 11 | 12 | @impl true 13 | def handle_playing(_ctx, state) do 14 | {[start_timer: {:timer, Time.milliseconds(100)}], state} 15 | end 16 | 17 | @impl true 18 | def handle_tick(:timer, _ctx, state) do 19 | {[notify_parent: :tick, stop_timer: :timer], state} 20 | end 21 | end 22 | 23 | defmodule Bin do 24 | use Membrane.Bin 25 | 26 | @impl true 27 | def handle_playing(_ctx, state) do 28 | {[start_timer: {:timer, Time.milliseconds(100)}], state} 29 | end 30 | 31 | @impl true 32 | def handle_tick(:timer, _ctx, state) do 33 | {[notify_parent: :tick, stop_timer: :timer], state} 34 | end 35 | end 36 | 37 | defmodule Pipeline do 38 | use Membrane.Pipeline 39 | 40 | @impl true 41 | def handle_init(_ctx, pid) do 42 | spec = [child(:element, Element), child(:bin, Bin)] 43 | 44 | {[spec: spec], %{pid: pid}} 45 | end 46 | 47 | @impl true 48 | def handle_playing(_ctx, state) do 49 | {[start_timer: {:timer, Time.milliseconds(100)}], state} 50 | end 51 | 52 | @impl true 53 | def handle_tick(:timer, _ctx, state) do 54 | send(state.pid, :pipeline_tick) 55 | {[stop_timer: :timer], state} 56 | end 57 | end 58 | 59 | test "Stopping timer from handle_tick" do 60 | pipeline = 61 | Testing.Pipeline.start_link_supervised!( 62 | module: Pipeline, 63 | custom_args: self() 64 | ) 65 | 66 | assert_pipeline_notified(pipeline, :element, :tick) 67 | assert_pipeline_notified(pipeline, :bin, :tick) 68 | assert_receive :pipeline_tick 69 | Testing.Pipeline.terminate(pipeline) 70 | end 71 | 72 | defmodule StopNoInterval do 73 | use Membrane.Source 74 | @impl true 75 | def handle_setup(_ctx, state) do 76 | Process.send_after(self(), :stop_timer, 0) 77 | {[start_timer: {:timer, :no_interval}], state} 78 | end 79 | 80 | @impl true 81 | def handle_info(:stop_timer, _ctx, state) do 82 | {[stop_timer: :timer, notify_parent: :ok], state} 83 | end 84 | end 85 | 86 | test "Stopping timer with `:no_interval`" do 87 | pipeline = Testing.Pipeline.start_link_supervised!(spec: [child(:element, StopNoInterval)]) 88 | 89 | assert_pipeline_notified(pipeline, :element, :ok) 90 | Testing.Pipeline.terminate(pipeline) 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /test/membrane/log_metadata_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Membrane.LogMetadataTest do 2 | use ExUnit.Case, async: true 3 | 4 | import Membrane.Testing.Assertions 5 | 6 | alias Membrane.Support.LogMetadataTest 7 | alias Membrane.Testing 8 | 9 | test "Custom log metadata are delivered to the correct element" do 10 | metadata_1 = "Metadata 1" 11 | metadata_2 = "Metadata 2" 12 | 13 | assert pipeline_pid = 14 | Testing.Pipeline.start_link_supervised!( 15 | module: LogMetadataTest.Pipeline, 16 | custom_args: %{elements: [element_1: metadata_1, element_2: metadata_2]} 17 | ) 18 | 19 | assert_pipeline_notified(pipeline_pid, :element_1, notification) 20 | 21 | assert Keyword.keyword?(notification) 22 | assert Keyword.has_key?(notification, :mb_prefix) 23 | assert Keyword.fetch!(notification, :test) == metadata_1 24 | 25 | assert_pipeline_notified(pipeline_pid, :element_2, notification) 26 | 27 | assert Keyword.keyword?(notification) 28 | assert Keyword.has_key?(notification, :mb_prefix) 29 | assert Keyword.fetch!(notification, :test) == metadata_2 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/membrane/pad_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Element.PadTest do 2 | use ExUnit.Case, async: true 3 | 4 | import Membrane.Pad 5 | 6 | describe "is_pad_ref/1" do 7 | test "when correct pad ref is given returns true" do 8 | assert is_pad_ref(:some_pad) 9 | end 10 | 11 | test "when correct dynamic pad ref is given returns true" do 12 | assert is_pad_ref(ref(:some_atom, make_ref())) 13 | end 14 | 15 | test "when incorrect dynamic pad ref is given returns false" do 16 | refute is_pad_ref({:dynamic, :some_atom, 3}) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/membrane/pipeline_supervisor_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Membrane.PipelineSupervisorTest do 2 | use ExUnit.Case, async: true 3 | 4 | import Membrane.ChildrenSpec 5 | import Membrane.Testing.Assertions 6 | 7 | alias Membrane.Testing 8 | 9 | test "Pipeline supervisor exits with the same reason as pipeline" do 10 | defmodule MyPipeline do 11 | use Membrane.Pipeline 12 | end 13 | 14 | {:ok, supervisor, pipeline} = Membrane.Pipeline.start(MyPipeline) 15 | 16 | supervisor_monitor_ref = Process.monitor(supervisor) 17 | pipeline_monitor_ref = Process.monitor(pipeline) 18 | 19 | exit_reason = :custom_exit_reason 20 | Process.exit(pipeline, exit_reason) 21 | 22 | assert_receive {:DOWN, ^pipeline_monitor_ref, _process, _pid, ^exit_reason} 23 | assert_receive {:DOWN, ^supervisor_monitor_ref, _process, _pid, ^exit_reason} 24 | end 25 | 26 | test "Pipeline supervisor exits with {:membrane_child_crash, child_name, child_exit_reason} when pipeline's child crashes" do 27 | defmodule MyElement do 28 | use Membrane.Endpoint 29 | 30 | @impl true 31 | def handle_playing(_ctx, state) do 32 | {[notify_parent: {:element_pid, self()}], state} 33 | end 34 | end 35 | 36 | {:ok, supervisor, pipeline} = Testing.Pipeline.start(spec: child(:element, MyElement)) 37 | 38 | assert_pipeline_notified(pipeline, :element, {:element_pid, element}) 39 | 40 | supervisor_monitor_ref = Process.monitor(supervisor) 41 | pipeline_monitor_ref = Process.monitor(pipeline) 42 | element_monitor_ref = Process.monitor(element) 43 | 44 | element_exit_reason = :custom_exit_reason 45 | Process.exit(element, element_exit_reason) 46 | 47 | pipeline_exit_reason = {:membrane_child_crash, :element, element_exit_reason} 48 | 49 | assert_receive {:DOWN, ^element_monitor_ref, _process, _pid, ^element_exit_reason} 50 | assert_receive {:DOWN, ^pipeline_monitor_ref, _process, _pid, ^pipeline_exit_reason} 51 | assert_receive {:DOWN, ^supervisor_monitor_ref, _process, _pid, ^pipeline_exit_reason} 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /test/membrane/pipeline_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Membrane.PipelineTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Membrane.Support.TrivialPipeline 5 | 6 | describe "Membrane.Pipeline.start_link/3 should" do 7 | test "return alive pid when starting a module implementing Membrane.Pipeline behaviour" do 8 | assert {:ok, supervisor_pid, pipeline_pid} = Membrane.Pipeline.start_link(TrivialPipeline) 9 | assert is_pid(supervisor_pid) == true 10 | assert is_pid(pipeline_pid) == true 11 | assert Process.alive?(supervisor_pid) == true 12 | assert Process.alive?(pipeline_pid) == true 13 | this_process_info = :erlang.process_info(self()) 14 | supervisor_info = :erlang.process_info(supervisor_pid) 15 | pipeline_info = :erlang.process_info(pipeline_pid) 16 | assert self() in supervisor_info[:links] 17 | assert supervisor_pid in this_process_info[:links] 18 | assert pipeline_pid in supervisor_info[:links] 19 | assert supervisor_pid in pipeline_info[:links] 20 | end 21 | 22 | test "return error tuple when starting a module that doesn't implement Membrane.Pipeline behaviour" do 23 | assert Membrane.Pipeline.start_link(NotAPipeline) == {:error, {:not_pipeline, NotAPipeline}} 24 | end 25 | end 26 | 27 | describe "Pipeline.start/3 should" do 28 | test "return alive pid when starting a module implementing Membrane.Pipeline behaviour" do 29 | assert {:ok, supervisor_pid, pipeline_pid} = Membrane.Pipeline.start(TrivialPipeline) 30 | assert is_pid(supervisor_pid) == true 31 | assert is_pid(pipeline_pid) == true 32 | assert Process.alive?(supervisor_pid) == true 33 | assert Process.alive?(pipeline_pid) == true 34 | this_process_info = :erlang.process_info(self()) 35 | supervisor_info = :erlang.process_info(supervisor_pid) 36 | pipeline_info = :erlang.process_info(pipeline_pid) 37 | assert self() not in supervisor_info[:links] 38 | assert supervisor_pid not in this_process_info[:links] 39 | assert pipeline_pid in supervisor_info[:links] 40 | assert supervisor_pid in pipeline_info[:links] 41 | end 42 | 43 | test "return error tuple when starting a module that doesn't implement Membrane.Pipeline behaviour" do 44 | assert Membrane.Pipeline.start(NotAPipeline) == {:error, {:not_pipeline, NotAPipeline}} 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /test/membrane/testing/dynamic_source_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Testing.DynamicSourceTest do 2 | use ExUnit.Case, async: true 3 | 4 | import Membrane.ChildrenSpec 5 | import Membrane.Testing.Assertions 6 | 7 | alias Membrane.Buffer 8 | alias Membrane.Testing 9 | 10 | test "Source initializes buffer generator and its state properly" do 11 | generator = fn _state, _size -> nil end 12 | 13 | assert {[], 14 | %{type: :generator, generator: ^generator, generator_state: :abc, state_for_pad: %{}}} = 15 | Testing.DynamicSource.handle_init(%{}, %Testing.DynamicSource{ 16 | output: {:abc, generator} 17 | }) 18 | end 19 | 20 | test "Source sends stream format on play" do 21 | assert {[stream_format: {:output, :stream_format}], _state} = 22 | Testing.DynamicSource.handle_playing(%{pads: %{:output => %{}}}, %{ 23 | stream_format: :stream_format 24 | }) 25 | end 26 | 27 | test "Source works properly when payload are passed as enumerable" do 28 | pipeline = 29 | Testing.Pipeline.start_link_supervised!( 30 | spec: 31 | [ 32 | child(:source, %Testing.DynamicSource{output: [~c"a", ~c"b", ~c"c"]}), 33 | child(:sink_1, Testing.Sink), 34 | child(:sink_2, Testing.Sink) 35 | ] ++ 36 | [ 37 | get_child(:source) |> get_child(:sink_1), 38 | get_child(:source) |> get_child(:sink_2) 39 | ] 40 | ) 41 | 42 | assert_sink_buffer(pipeline, :sink_1, %Buffer{payload: ~c"a"}) 43 | assert_sink_buffer(pipeline, :sink_1, %Buffer{payload: ~c"b"}) 44 | assert_sink_buffer(pipeline, :sink_1, %Buffer{payload: ~c"c"}) 45 | assert_sink_buffer(pipeline, :sink_2, %Buffer{payload: ~c"a"}) 46 | assert_sink_buffer(pipeline, :sink_2, %Buffer{payload: ~c"b"}) 47 | assert_sink_buffer(pipeline, :sink_2, %Buffer{payload: ~c"c"}) 48 | end 49 | 50 | test "Source works properly when using generator function" do 51 | pipeline = 52 | Testing.Pipeline.start_link_supervised!( 53 | spec: [ 54 | child(:source, Testing.DynamicSource) 55 | |> child(:sink_1, Testing.Sink), 56 | get_child(:source) 57 | |> child(:sink_2, Testing.Sink) 58 | ] 59 | ) 60 | 61 | assert_sink_buffer(pipeline, :sink_1, %Buffer{payload: <<0::16>>}) 62 | assert_sink_buffer(pipeline, :sink_1, %Buffer{payload: <<1::16>>}) 63 | assert_sink_buffer(pipeline, :sink_1, %Buffer{payload: <<2::16>>}) 64 | assert_sink_buffer(pipeline, :sink_2, %Buffer{payload: <<0::16>>}) 65 | assert_sink_buffer(pipeline, :sink_2, %Buffer{payload: <<1::16>>}) 66 | assert_sink_buffer(pipeline, :sink_2, %Buffer{payload: <<2::16>>}) 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /test/membrane/testing/mock_resource_guard_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Testing.MockResourceGuardTest do 2 | use ExUnit.Case, async: true 3 | 4 | doctest Membrane.Testing.MockResourceGuard 5 | end 6 | -------------------------------------------------------------------------------- /test/membrane/testing/sink_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Testing.SinkTest do 2 | use ExUnit.Case, async: true 3 | 4 | alias Membrane.Testing.Notification 5 | alias Membrane.Testing.Sink 6 | 7 | describe "Handle buffer" do 8 | test "demands when autodemand is true" do 9 | buffer = %Membrane.Buffer{payload: 123} 10 | 11 | assert {actions, _state} = Sink.handle_buffer(:input, buffer, nil, %{autodemand: true}) 12 | 13 | assert actions == [ 14 | demand: :input, 15 | notify_parent: %Notification{payload: {:buffer, buffer}} 16 | ] 17 | end 18 | 19 | test "does not demand when autodemand is false" do 20 | buffer = %Membrane.Buffer{payload: 123} 21 | 22 | assert {actions, _state} = Sink.handle_buffer(:input, buffer, nil, %{autodemand: false}) 23 | 24 | assert actions == [notify_parent: %Notification{payload: {:buffer, buffer}}] 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/membrane/time_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Membrane.TimeTest do 2 | use ExUnit.Case, async: true 3 | 4 | @module Membrane.Time 5 | 6 | # Apr 20, 2020 11:23:52.178157999 7 | @ntp_timestamp <<3_796_370_632::32, 765_182_783::32>> 8 | @timestamp_unix_ns 1_587_381_832_178_157_999 9 | 10 | doctest @module 11 | 12 | test "conversion from NTP" do 13 | timestamp = @ntp_timestamp |> @module.from_ntp_timestamp() 14 | assert_in_delta timestamp, @timestamp_unix_ns, 1 15 | end 16 | 17 | test "conversion to NTP " do 18 | unix_timestamp = @timestamp_unix_ns |> @module.nanoseconds() 19 | assert <> = unix_timestamp |> @module.to_ntp_timestamp() 20 | 21 | <> = @ntp_timestamp 22 | assert seconds == ref_seconds 23 | # 10 decimal means 10 / 2^32 delta, that is ~ 2.33 nanoseconds 24 | assert_in_delta fraction, ref_fraction, 10 25 | end 26 | 27 | test "NTP convesion error " do 28 | regenerated_timestamp = 29 | @timestamp_unix_ns |> @module.to_ntp_timestamp() |> @module.from_ntp_timestamp() 30 | 31 | assert_in_delta @timestamp_unix_ns, regenerated_timestamp, 1 32 | end 33 | 34 | test "Time units" do 35 | value = 123 36 | assert @module.nanoseconds(value) == value 37 | assert @module.microseconds(value) == value * 1000 38 | assert @module.milliseconds(value) == value * 1_000_000 39 | assert @module.seconds(value) == value * 1_000_000_000 40 | assert @module.hours(value) == value * 3_600_000_000_000 41 | assert @module.days(value) == value * 86_400_000_000_000 42 | 43 | assert value |> @module.seconds() |> @module.native_units() == 44 | :erlang.convert_time_unit(value, :seconds, :native) 45 | end 46 | 47 | test "Monotonic time should be integer" do 48 | assert is_integer(@module.monotonic_time()) 49 | end 50 | 51 | test "Time units functions work properly with rational numbers" do 52 | value = Ratio.new(1, 2) 53 | assert @module.microseconds(value) == 500 54 | assert @module.milliseconds(value) == 500_000 55 | assert @module.seconds(value) == 500_000_000 56 | assert @module.hours(value) == 1_800_000_000_000 57 | assert @module.days(value) == 43_200_000_000_000 58 | end 59 | 60 | test "Time units functions properly round the values" do 61 | assert @module.seconds(Ratio.new(1, 3)) == 333_333_333 62 | assert @module.seconds(Ratio.new(-1, 3)) == -333_333_333 63 | 64 | assert @module.seconds(Ratio.new(2, 3)) == 666_666_667 65 | assert @module.seconds(Ratio.new(-2, 3)) == -666_666_667 66 | end 67 | 68 | test "Time.to_timebase/2 works properly" do 69 | assert @module.divide_by_timebase(4, 2) == 2 70 | assert @module.divide_by_timebase(3, Ratio.new(3, 2)) == 2 71 | assert @module.divide_by_timebase(Ratio.new(15, 2), 2) == 4 72 | assert @module.divide_by_timebase(Ratio.new(15, 2), Ratio.new(3, 2)) == 5 73 | assert @module.divide_by_timebase(4, 10) == 0 74 | assert @module.divide_by_timebase(4, 7) == 1 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /test/membrane/utility_supervisor_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Membrane.UtilitySupervisorTest do 2 | use ExUnit.Case, async: true 3 | 4 | import Membrane.ChildrenSpec 5 | import Membrane.Testing.Assertions 6 | 7 | alias Membrane.Testing 8 | 9 | test "Utility supervisor terminates utility when element exits" do 10 | Process.register(self(), :utility_supervisor_test_process) 11 | 12 | defmodule TestFilter do 13 | use Membrane.Filter 14 | 15 | @impl true 16 | def handle_setup(ctx, state) do 17 | Membrane.UtilitySupervisor.start_link_child( 18 | ctx.utility_supervisor, 19 | {Task, 20 | fn -> 21 | send(:utility_supervisor_test_process, {:task_pid, self()}) 22 | Process.sleep(:infinity) 23 | end} 24 | ) 25 | 26 | {[notify_parent: :setup], state} 27 | end 28 | end 29 | 30 | pipeline = Testing.Pipeline.start_supervised!(spec: [child(:filter, TestFilter)]) 31 | 32 | assert_pipeline_notified(pipeline, :filter, :setup) 33 | assert_receive {:task_pid, task_pid} 34 | 35 | monitor_ref = Process.monitor(task_pid) 36 | 37 | Testing.Pipeline.terminate(pipeline) 38 | assert_receive {:DOWN, ^monitor_ref, :process, _pid, :shutdown} 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /test/support/accepted_format_test/inner_sink_bin.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Support.AcceptedFormatTest.InnerSinkBin do 2 | @moduledoc """ 3 | Bin used in accepted format tests. 4 | It has a :accepted_format defined for the `:input` pad. 5 | Spawns `Membrane.Support.AcceptedFormatTest.Sink` as its child. 6 | """ 7 | 8 | use Membrane.Bin 9 | 10 | alias Membrane.Support.AcceptedFormatTest 11 | alias Membrane.Support.AcceptedFormatTest.StreamFormat 12 | alias Membrane.Support.AcceptedFormatTest.StreamFormat.{AcceptedByAll, AcceptedByInnerBins} 13 | 14 | def_input_pad :input, 15 | accepted_format: 16 | %StreamFormat{format: format} when format in [AcceptedByAll, AcceptedByInnerBins], 17 | availability: :always 18 | 19 | def_options test_pid: [type: :pid] 20 | 21 | @impl true 22 | def handle_init(_ctx, %__MODULE__{test_pid: test_pid}) do 23 | spec = 24 | bin_input() 25 | |> child(:sink, %AcceptedFormatTest.Sink{test_pid: test_pid}) 26 | 27 | {[spec: spec], %{}} 28 | end 29 | 30 | @impl true 31 | def handle_child_notification(msg, _child, _ctx, state) do 32 | {[notify_parent: msg], state} 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/support/accepted_format_test/inner_source_bin.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Support.AcceptedFormatTest.InnerSourceBin do 2 | @moduledoc """ 3 | Bin used in accepted format tests. 4 | It has a :accepted_format defined for the `:output` pad. 5 | Spawns `Membrane.Support.AcceptedFormatTest.Source` as its child. 6 | """ 7 | 8 | use Membrane.Bin 9 | 10 | alias Membrane.Support.AcceptedFormatTest 11 | alias Membrane.Support.AcceptedFormatTest.StreamFormat 12 | alias Membrane.Support.AcceptedFormatTest.StreamFormat.{AcceptedByAll, AcceptedByInnerBins} 13 | 14 | def_output_pad :output, 15 | accepted_format: 16 | %StreamFormat{format: format} when format in [AcceptedByAll, AcceptedByInnerBins] 17 | 18 | def_options test_pid: [type: :pid], 19 | stream_format: [type: :any] 20 | 21 | @impl true 22 | def handle_init(_ctx, %__MODULE__{test_pid: test_pid, stream_format: stream_format}) do 23 | spec = 24 | child(:source, %AcceptedFormatTest.Source{ 25 | test_pid: test_pid, 26 | stream_format: stream_format 27 | }) 28 | |> bin_output() 29 | 30 | {[spec: spec], %{}} 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/support/accepted_format_test/outer_sink_bin.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Support.AcceptedFormatTest.OuterSinkBin do 2 | @moduledoc """ 3 | Bin used in accepted format tests. 4 | It has a :accepted_format defined for the `:input` pad. 5 | Spawns `Membrane.Support.AcceptedFormatTest.InnerSinkBin` as its child. 6 | """ 7 | 8 | use Membrane.Bin 9 | 10 | alias Membrane.Support.AcceptedFormatTest 11 | alias Membrane.Support.AcceptedFormatTest.StreamFormat 12 | alias Membrane.Support.AcceptedFormatTest.StreamFormat.{AcceptedByAll, AcceptedByOuterBins} 13 | 14 | def_input_pad :input, 15 | accepted_format: 16 | any_of(%StreamFormat{format: AcceptedByAll}, %StreamFormat{format: AcceptedByOuterBins}), 17 | availability: :always 18 | 19 | def_options test_pid: [type: :pid] 20 | 21 | @impl true 22 | def handle_init(_ctx, %__MODULE__{test_pid: test_pid}) do 23 | spec = 24 | bin_input() 25 | |> child(:sink, %AcceptedFormatTest.InnerSinkBin{test_pid: test_pid}) 26 | 27 | {[spec: spec], %{}} 28 | end 29 | 30 | @impl true 31 | def handle_child_notification(msg, _child, _ctx, state) do 32 | {[notify_parent: msg], state} 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/support/accepted_format_test/outer_source_bin.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Support.AcceptedFormatTest.OuterSourceBin do 2 | @moduledoc """ 3 | Bin used in accepted format tests. 4 | It has a accepted_format defined for the `:output` pad. 5 | Spawns `Membrane.Support.AcceptedFormatTest.InnerSourceBin` as its child. 6 | """ 7 | 8 | use Membrane.Bin 9 | 10 | alias Membrane.Support.AcceptedFormatTest 11 | alias Membrane.Support.AcceptedFormatTest.StreamFormat 12 | alias Membrane.Support.AcceptedFormatTest.StreamFormat.{AcceptedByAll, AcceptedByOuterBins} 13 | 14 | def_output_pad :output, 15 | accepted_format: 16 | %StreamFormat{format: format} when format in [AcceptedByAll, AcceptedByOuterBins] 17 | 18 | def_options test_pid: [type: :pid], 19 | stream_format: [type: :any] 20 | 21 | @impl true 22 | def handle_init(_ctx, %__MODULE__{test_pid: test_pid, stream_format: stream_format}) do 23 | spec = 24 | child(:inner_source_bin, %AcceptedFormatTest.InnerSourceBin{ 25 | test_pid: test_pid, 26 | stream_format: stream_format 27 | }) 28 | |> bin_output() 29 | 30 | {[spec: spec], %{}} 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/support/accepted_format_test/restrictive_sink.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Support.AcceptedFormatTest.RestrictiveSink do 2 | @moduledoc """ 3 | Sink used in accepted format tests. 4 | Sends a message with its own pid to the process specified in the options. 5 | Notifies parent on stream format arrival. 6 | """ 7 | 8 | use Membrane.Endpoint 9 | 10 | alias Membrane.Support.AcceptedFormatTest.StreamFormat 11 | 12 | def_input_pad :input, 13 | accepted_format: %StreamFormat{format: StreamFormat.AcceptedByAll}, 14 | availability: :always, 15 | flow_control: :push 16 | 17 | def_options test_pid: [type: :pid] 18 | 19 | @impl true 20 | def handle_init(_ctx, %__MODULE__{test_pid: test_pid}) do 21 | send(test_pid, {:my_pid, __MODULE__, self()}) 22 | {[], %{test_pid: test_pid}} 23 | end 24 | 25 | @impl true 26 | def handle_stream_format(:input, stream_format, _ctx, state) do 27 | {[notify_parent: {:stream_format_received, stream_format}], state} 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/support/accepted_format_test/restrictive_source.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Support.AcceptedFormatTest.RestrictiveSource do 2 | @moduledoc """ 3 | Source used in accepted format tests. 4 | Sends stream format passed in opts, after entering the `:playing` playback. 5 | """ 6 | 7 | use Membrane.Source 8 | 9 | alias Membrane.Support.AcceptedFormatTest.StreamFormat 10 | 11 | def_output_pad :output, 12 | accepted_format: %StreamFormat{format: StreamFormat.AcceptedByAll}, 13 | availability: :always, 14 | flow_control: :push 15 | 16 | def_options test_pid: [type: :pid], 17 | stream_format: [type: :any] 18 | 19 | @impl true 20 | def handle_init(_ctx, %__MODULE__{test_pid: test_pid, stream_format: stream_format}) do 21 | {[], %{test_pid: test_pid, stream_format: stream_format}} 22 | end 23 | 24 | @impl true 25 | def handle_playing(_ctx, state) do 26 | send(state.test_pid, {:my_pid, __MODULE__, self()}) 27 | {[], state} 28 | end 29 | 30 | @impl true 31 | def handle_info(:send_stream_format, _ctx, state) do 32 | {[stream_format: {:output, state.stream_format}], state} 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/support/accepted_format_test/sink.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Support.AcceptedFormatTest.Sink do 2 | @moduledoc """ 3 | Sink used in accepted format tests. 4 | Sends a message with its own pid to the process specified in the options. 5 | Notifies parent on stream format arrival. 6 | """ 7 | 8 | use Membrane.Sink 9 | 10 | alias Membrane.Support.AcceptedFormatTest.StreamFormat 11 | 12 | def_input_pad :input, 13 | accepted_format: StreamFormat, 14 | availability: :always, 15 | flow_control: :push 16 | 17 | def_options test_pid: [type: :pid] 18 | 19 | @impl true 20 | def handle_init(_ctx, %__MODULE__{test_pid: test_pid}) do 21 | send(test_pid, {:my_pid, __MODULE__, self()}) 22 | {[], %{test_pid: test_pid}} 23 | end 24 | 25 | @impl true 26 | def handle_stream_format(:input, stream_format, _ctx, state) do 27 | {[notify_parent: {:stream_format_received, stream_format}], state} 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/support/accepted_format_test/source.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Support.AcceptedFormatTest.Source do 2 | @moduledoc """ 3 | Source used in accepted format tests. 4 | Sends stream format passed in opts, after entering the `:playing` playback. 5 | """ 6 | 7 | use Membrane.Source 8 | 9 | alias Membrane.Support.AcceptedFormatTest.StreamFormat 10 | 11 | def_output_pad :output, 12 | accepted_format: StreamFormat, 13 | availability: :always, 14 | flow_control: :push 15 | 16 | def_options test_pid: [type: :pid], 17 | stream_format: [type: :any] 18 | 19 | @impl true 20 | def handle_init(_ctx, %__MODULE__{test_pid: test_pid, stream_format: stream_format}) do 21 | {[], %{test_pid: test_pid, stream_format: stream_format}} 22 | end 23 | 24 | @impl true 25 | def handle_playing(_ctx, state) do 26 | send(state.test_pid, {:my_pid, __MODULE__, self()}) 27 | {[], state} 28 | end 29 | 30 | @impl true 31 | def handle_info(:send_stream_format, _ctx, state) do 32 | {[stream_format: {:output, state.stream_format}], state} 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/support/accepted_format_test/stream.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Support.AcceptedFormatTest.StreamFormat do 2 | @moduledoc """ 3 | Stream formats definitions used in accepted format tests. 4 | """ 5 | 6 | @type t() :: %__MODULE__{ 7 | format: 8 | nil | FormatAcceptedByAll | FormatAcceptedByOuterBins | FormatAcceptedByInnerBins 9 | } 10 | 11 | defstruct [:format] 12 | 13 | defmodule AcceptedByAll do 14 | @moduledoc """ 15 | Stream format definition used in stream format tests. 16 | Accepted by all bins used in stream format tests. 17 | """ 18 | end 19 | 20 | defmodule AcceptedByOuterBins do 21 | @moduledoc """ 22 | Stream format definition used in stream format test. 23 | Accepted by outer bins used in stream format tests. 24 | """ 25 | end 26 | 27 | defmodule AcceptedByInnerBins do 28 | @moduledoc """ 29 | Stream format definition used in stream format test. 30 | Accepted by inner bins used in stream format tests. 31 | """ 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/support/child_crash_test/filter.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Support.ChildCrashTest.Filter do 2 | @moduledoc """ 3 | Filter used in child crash test. 4 | Can be crashed on demand by sending `:crash` message. 5 | """ 6 | 7 | use Membrane.Filter 8 | 9 | alias Membrane.Pad 10 | 11 | def_output_pad :output, flow_control: :manual, accepted_format: _any, availability: :on_request 12 | 13 | def_input_pad :input, 14 | flow_control: :manual, 15 | demand_unit: :buffers, 16 | accepted_format: _any, 17 | availability: :on_request 18 | 19 | @impl true 20 | def handle_init(_ctx, _opts) do 21 | state = %{ 22 | input_pads: MapSet.new(), 23 | output_pads: MapSet.new() 24 | } 25 | 26 | {[], state} 27 | end 28 | 29 | @impl true 30 | def handle_pad_added(Pad.ref(name, _ref) = pad, _ctx, state) do 31 | key = 32 | case name do 33 | :output -> :output_pads 34 | :input -> :input_pads 35 | end 36 | 37 | state = Map.update!(state, key, &MapSet.put(&1, pad)) 38 | 39 | {[], state} 40 | end 41 | 42 | @impl true 43 | def handle_demand(Pad.ref(:output, _pad_ref), size, _unit, _ctx, state) do 44 | demands = 45 | state.input_pads 46 | |> Enum.map(fn pad -> {:demand, {pad, size}} end) 47 | 48 | {demands, state} 49 | end 50 | 51 | @impl true 52 | def handle_info(:crash, _ctx, state) do 53 | # code that will cause crash of the filter 54 | Process.exit(self(), :crash) 55 | 56 | {[], state} 57 | end 58 | 59 | @impl true 60 | def handle_buffer(_pad, buf, _ctx, state) do 61 | actions = 62 | for pad <- state.output_pads do 63 | {:buffer, {pad, buf}} 64 | end 65 | 66 | {actions, state} 67 | end 68 | 69 | @impl true 70 | def handle_end_of_stream(pad, _ctx, state) do 71 | {[], %{state | input_pads: MapSet.delete(state.input_pads, pad)}} 72 | end 73 | 74 | @spec crash(pid()) :: any() 75 | def crash(pid) do 76 | send(pid, :crash) 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /test/support/child_removal_test/child_removing_pipeline.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Support.ChildRemovalTest.ChildRemovingPipeline do 2 | @moduledoc false 3 | use Membrane.Pipeline 4 | 5 | import Membrane.ChildrenSpec 6 | 7 | alias Membrane.Support.ChildRemovalTest.FilterToBeRemoved 8 | alias Membrane.Support.ChildRemovalTest.SourceNotyfingWhenPadRemoved 9 | 10 | @impl true 11 | def handle_init(_ctx, _opts) do 12 | spec1 = [child(:source, SourceNotyfingWhenPadRemoved)] 13 | 14 | spec2 = [ 15 | get_child(:source) |> via_out(:first) |> child(:filter1, FilterToBeRemoved), 16 | get_child(:source) |> via_out(:second) |> child(:filter2, FilterToBeRemoved) 17 | ] 18 | 19 | spec2 = {spec2, group: :first_crash_group, crash_group_mode: :temporary} 20 | 21 | spec3 = [get_child(:source) |> via_out(:third) |> child(:filter3, FilterToBeRemoved)] 22 | spec3 = {spec3, group: :first_crash_group, crash_group_mode: :temporary} 23 | 24 | spec4 = [ 25 | get_child(:source) |> via_out(:fourth) |> child(:filter4, FilterToBeRemoved), 26 | get_child(:source) |> via_out(:fifth) |> child(:filter5, FilterToBeRemoved) 27 | ] 28 | 29 | spec4 = {spec4, group: :second_crash_group, crash_group_mode: :temporary} 30 | 31 | {[spec: spec1, spec: spec2, spec: spec3, spec: spec4], %{}} 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/support/child_removal_test/filter.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Support.ChildRemovalTest.Filter do 2 | @moduledoc """ 3 | Module used in tests for elements removing. 4 | 5 | It allows to: 6 | * slow down the moment of switching to :playing. 7 | * send demands and buffers from two input pads to one output pad. 8 | 9 | Should be used along with `Membrane.Support.ChildRemovalTest.Pipeline` as they 10 | share names (i.e. input_pads: `input1` and `input2`) and exchanged messages' formats. 11 | """ 12 | 13 | use Membrane.Filter 14 | 15 | def_output_pad :output, flow_control: :manual, accepted_format: _any, availability: :on_request 16 | 17 | def_input_pad :input1, 18 | flow_control: :manual, 19 | demand_unit: :buffers, 20 | accepted_format: _any, 21 | availability: :on_request 22 | 23 | def_input_pad :input2, 24 | flow_control: :manual, 25 | demand_unit: :buffers, 26 | accepted_format: _any, 27 | availability: :on_request 28 | 29 | def_options demand_generator: [ 30 | spec: (pos_integer -> non_neg_integer), 31 | default: &__MODULE__.default_demand_generator/1 32 | ], 33 | playing_delay: [spec: integer(), default: 0] 34 | 35 | @impl true 36 | def handle_init(_ctx, opts) do 37 | {[], Map.put(opts, :pads, MapSet.new())} 38 | end 39 | 40 | @impl true 41 | def handle_pad_added(pad, _ctx, state) do 42 | new_pads = MapSet.put(state.pads, pad) 43 | {[], %{state | pads: new_pads}} 44 | end 45 | 46 | @impl true 47 | def handle_pad_removed(pad, _ctx, state) do 48 | new_pads = MapSet.delete(state.pads, pad) 49 | {[], %{state | pads: new_pads}} 50 | end 51 | 52 | @impl true 53 | def handle_playing(_ctx, %{playing_delay: time} = state) do 54 | Process.sleep(time) 55 | {[notify_parent: :playing], state} 56 | end 57 | 58 | @impl true 59 | def handle_demand(_output, size, _unit, ctx, state) do 60 | demands = 61 | ctx.pads 62 | |> Map.values() 63 | |> Enum.filter(&(&1.direction == :input)) 64 | |> Enum.map(fn pad -> {:demand, {pad.ref, state.demand_generator.(size)}} end) 65 | 66 | {demands, state} 67 | end 68 | 69 | @impl true 70 | def handle_buffer(_input, buf, ctx, state) do 71 | buffers = 72 | ctx.pads 73 | |> Map.values() 74 | |> Enum.filter(&(&1.direction == :output)) 75 | |> Enum.map(&{:buffer, {&1.ref, buf}}) 76 | 77 | {buffers, state} 78 | end 79 | 80 | @impl true 81 | def handle_end_of_stream(pad, _ctx, state) do 82 | {[], %{state | pads: MapSet.delete(state.pads, pad)}} 83 | end 84 | 85 | @spec default_demand_generator(integer()) :: integer() 86 | def default_demand_generator(demand), do: demand 87 | end 88 | -------------------------------------------------------------------------------- /test/support/child_removal_test/filter_to_be_removed.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Support.ChildRemovalTest.FilterToBeRemoved do 2 | @moduledoc false 3 | use Membrane.Filter, flow_control_hints?: false 4 | 5 | def_input_pad :input, accepted_format: _any, flow_control: :auto, availability: :on_request 6 | def_output_pad :output, accepted_format: _any, flow_control: :auto, availability: :on_request 7 | 8 | @impl true 9 | def handle_init(_ctx, _opts) do 10 | {[], %{}} 11 | end 12 | 13 | @impl true 14 | def handle_buffer(:input, buffers, _context, state) do 15 | {[buffer: {:output, buffers}], state} 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/support/child_removal_test/pipeline.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Support.ChildRemovalTest.Pipeline do 2 | @moduledoc """ 3 | Module used in tests for elements removing. 4 | 5 | This module allows to build two pipelines: 6 | * Simple one, with two filters 7 | source -- filter1 -- [input1] filter2 -- sink 8 | * Pipeline with two sources (if `extra_source` key is provided in opts). 9 | source -- filter1 -- filter2 -- [input1] filter3 -- sink 10 | [input2] 11 | / 12 | extra_source ___/ 13 | 14 | Should be used along with `Membrane.Support.ChildRemovalTest.Pipeline` as they 15 | share names (i.e. input_pads: `input1` and `input2`) and exchanged messages' formats. 16 | """ 17 | use Membrane.Pipeline 18 | 19 | @spec remove_child(pid(), Membrane.Child.name()) :: any() 20 | def remove_child(pid, child_name) do 21 | send(pid, {:remove_children, child_name}) 22 | end 23 | 24 | @impl true 25 | def handle_init(_ctx, opts) do 26 | children = 27 | [ 28 | child(:source, opts.source), 29 | child(:filter1, opts.filter1), 30 | child(:filter2, opts.filter2), 31 | child(:filter3, opts.filter3), 32 | child(:sink, opts.sink) 33 | ] 34 | |> maybe_add_extra_source(opts) 35 | 36 | links = 37 | [ 38 | get_child(:source) 39 | |> via_in(:input1, target_queue_size: 10) 40 | |> get_child(:filter1) 41 | |> via_in(:input1, target_queue_size: 10) 42 | |> get_child(:filter2) 43 | |> via_in(:input1, target_queue_size: 10) 44 | |> get_child(:filter3) 45 | |> via_in(:input, target_queue_size: 10) 46 | |> get_child(:sink) 47 | ] 48 | |> maybe_add_extra_source_link(opts) 49 | 50 | spec = links ++ children 51 | 52 | {[spec: spec], %{}} 53 | end 54 | 55 | @impl true 56 | def handle_info({:child_msg, name, msg}, _ctx, state) do 57 | {[notify_child: {name, msg}], state} 58 | end 59 | 60 | @impl true 61 | def handle_info({:remove_children, name}, _ctx, state) do 62 | {[remove_children: name], state} 63 | end 64 | 65 | defp maybe_add_extra_source(children, %{extra_source: source}), 66 | do: [child(:extra_source, source) | children] 67 | 68 | defp maybe_add_extra_source(children, _opts), do: children 69 | 70 | defp maybe_add_extra_source_link(links, %{extra_source: _}) do 71 | [ 72 | get_child(:extra_source) |> via_in(:input2, target_queue_size: 10) |> get_child(:filter3) 73 | | links 74 | ] 75 | end 76 | 77 | defp maybe_add_extra_source_link(links, _opts) do 78 | links 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /test/support/child_removal_test/source_notyfing_when_pad_removed.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Support.ChildRemovalTest.SourceNotyfingWhenPadRemoved do 2 | @moduledoc false 3 | use Membrane.Source 4 | 5 | def_output_pad :first, accepted_format: _any, flow_control: :push, availability: :on_request 6 | def_output_pad :second, accepted_format: _any, flow_control: :push, availability: :on_request 7 | def_output_pad :third, accepted_format: _any, flow_control: :push, availability: :on_request 8 | def_output_pad :fourth, accepted_format: _any, flow_control: :push, availability: :on_request 9 | def_output_pad :fifth, accepted_format: _any, flow_control: :push, availability: :on_request 10 | 11 | @impl true 12 | def handle_init(_ctx, _opts) do 13 | {[], %{}} 14 | end 15 | 16 | @impl true 17 | def handle_pad_removed(pad, _ctx, state) do 18 | {[notify_parent: {:pad_removed, pad}], state} 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/support/demands_test/filter.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Support.DemandsTest.Filter do 2 | @moduledoc false 3 | use Membrane.Filter 4 | 5 | alias Membrane.Buffer 6 | 7 | def_output_pad :output, flow_control: :manual, accepted_format: _any 8 | 9 | def_input_pad :input, flow_control: :manual, demand_unit: :buffers, accepted_format: _any 10 | 11 | def_options demand_generator: [ 12 | spec: (pos_integer -> non_neg_integer), 13 | default: &__MODULE__.default_demand_generator/1 14 | ] 15 | 16 | @impl true 17 | def handle_init(_ctx, opts) do 18 | {[], opts} 19 | end 20 | 21 | @impl true 22 | def handle_demand(:output, size, _unit, _ctx, state) do 23 | {[demand: {:input, state.demand_generator.(size)}], state} 24 | end 25 | 26 | @impl true 27 | def handle_buffer(:input, %Buffer{payload: payload}, _ctx, state) do 28 | {[buffer: {:output, %Buffer{payload: payload <> <<255>>}}, redemand: :output], state} 29 | end 30 | 31 | @spec default_demand_generator(integer()) :: integer() 32 | def default_demand_generator(demand), do: demand 33 | end 34 | -------------------------------------------------------------------------------- /test/support/demands_test/pipeline.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Support.DemandsTest.Pipeline do 2 | @moduledoc false 3 | use Membrane.Pipeline 4 | 5 | @impl true 6 | def handle_init(_ctx, opts) do 7 | children = [ 8 | source: opts.source, 9 | filter: opts.filter, 10 | sink: opts.sink 11 | ] 12 | 13 | links = [ 14 | get_child(:source) 15 | |> via_in(:input, target_queue_size: 50) 16 | |> get_child(:filter) 17 | |> via_in(:input, target_queue_size: 50) 18 | |> get_child(:sink) 19 | ] 20 | 21 | spec = children ++ links 22 | 23 | {[spec: spec], %{target: opts.target}} 24 | end 25 | 26 | @impl true 27 | def handle_info({:child_msg, name, msg}, _ctx, state) do 28 | {[notify_child: {name, msg}], state} 29 | end 30 | 31 | @impl true 32 | def handle_playing(_ctx, %{target: target} = state) do 33 | send(target, :playing) 34 | {[], state} 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /test/support/distributed.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Support.Distributed do 2 | @moduledoc false 3 | 4 | defmodule SomeStreamFormat do 5 | @moduledoc false 6 | defstruct [] 7 | end 8 | 9 | defmodule Source do 10 | @moduledoc false 11 | use Membrane.Source 12 | 13 | def_output_pad :output, accepted_format: _any, flow_control: :push 14 | def_options output: [spec: list(any())] 15 | 16 | @impl true 17 | def handle_init(_ctx, opts) do 18 | {[], opts.output} 19 | end 20 | 21 | @impl true 22 | def handle_playing(_ctx, list) do 23 | stream_format = %SomeStreamFormat{} 24 | 25 | {[ 26 | stream_format: {:output, stream_format}, 27 | start_timer: {:timer, Membrane.Time.milliseconds(100)} 28 | ], list} 29 | end 30 | 31 | @impl true 32 | def handle_tick(_timer_id, _context, [first | rest]) do 33 | {[buffer: {:output, %Membrane.Buffer{payload: first}}], rest} 34 | end 35 | 36 | @impl true 37 | def handle_tick(_timer_id, _context, []) do 38 | {[end_of_stream: :output, stop_timer: :timer], []} 39 | end 40 | end 41 | 42 | defmodule Sink do 43 | @moduledoc false 44 | use Membrane.Sink 45 | 46 | def_input_pad :input, flow_control: :manual, accepted_format: _any, demand_unit: :buffers 47 | 48 | @impl true 49 | def handle_playing(_ctx, state) do 50 | {[demand: {:input, 1}], state} 51 | end 52 | 53 | @impl true 54 | def handle_buffer(_pad, _buffer, _ctx, state) do 55 | {[demand: {:input, 1}], state} 56 | end 57 | end 58 | 59 | defmodule SinkBin do 60 | @moduledoc false 61 | use Membrane.Bin 62 | 63 | def_input_pad :input, accepted_format: _any 64 | 65 | @impl true 66 | def handle_init(_ctx, _opts) do 67 | spec = bin_input() |> child(:sink, Sink) 68 | {[spec: spec], %{}} 69 | end 70 | 71 | @impl true 72 | def handle_element_end_of_stream(:sink, :input, _ctx, state) do 73 | {[notify_parent: :end_of_stream], state} 74 | end 75 | end 76 | 77 | defmodule Pipeline do 78 | @moduledoc false 79 | use Membrane.Pipeline 80 | 81 | @impl true 82 | def handle_init(_ctx, opts) do 83 | first_node = opts.first_node 84 | second_node = opts.second_node 85 | 86 | {[ 87 | spec: [ 88 | {child(:source, %Source{output: [1, 2, 3, 4, 5]}), node: first_node}, 89 | { 90 | get_child(:source) |> child(:sink_bin, SinkBin), 91 | node: second_node 92 | } 93 | ] 94 | ], %{}} 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /test/support/dynamic_filter.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Support.Element.DynamicFilter do 2 | @moduledoc """ 3 | This is a mock filter with dynamic inputs for use in specs. 4 | 5 | Modify with caution as many specs may depend on its shape. 6 | """ 7 | 8 | use Bunch 9 | use Membrane.Filter, flow_control_hints?: false 10 | 11 | def_input_pad :input, accepted_format: _any, availability: :on_request, flow_control: :auto 12 | def_output_pad :output, accepted_format: _any, availability: :on_request, flow_control: :auto 13 | 14 | @impl true 15 | def handle_init(_ctx, _options) do 16 | {[], %{}} 17 | end 18 | 19 | @impl true 20 | def handle_pad_added(pad, _ctx, state) do 21 | {[notify_parent: {:pad_added, pad}], state |> Map.put(:last_pad_addded, pad)} 22 | end 23 | 24 | @impl true 25 | def handle_pad_removed(pad, _ctx, state) do 26 | {[notify_parent: {:pad_removed, pad}], state |> Map.put(:last_pad_removed, pad)} 27 | end 28 | 29 | @impl true 30 | def handle_event(ref, event, _ctx, state) do 31 | {[forward: event], state |> Map.put(:last_event, {ref, event})} 32 | end 33 | 34 | @impl true 35 | def handle_end_of_stream(_pad_ref, ctx, state) do 36 | actions = 37 | Enum.flat_map(ctx.pads, fn 38 | {pad_ref, %{direction: :output, end_of_stream?: false}} -> [end_of_stream: pad_ref] 39 | _other -> [] 40 | end) 41 | 42 | {actions, state} 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /test/support/element/trivial_filter.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Support.Element.TrivialFilter do 2 | @moduledoc """ 3 | This is the most basic filter. It does nothing, but is used in tests. 4 | """ 5 | 6 | use Membrane.Filter 7 | 8 | def_output_pad :output, flow_control: :manual, accepted_format: _any 9 | 10 | def_input_pad :input, flow_control: :manual, accepted_format: _any, demand_unit: :buffers 11 | end 12 | -------------------------------------------------------------------------------- /test/support/element/trivial_sink.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Support.Element.TrivialSink do 2 | @moduledoc """ 3 | This is the most basic sink. It does nothing, but is used in tests. 4 | """ 5 | 6 | use Membrane.Sink 7 | 8 | def_input_pad :input, flow_control: :manual, accepted_format: _any, demand_unit: :buffers 9 | end 10 | -------------------------------------------------------------------------------- /test/support/element/trivial_source.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Support.Element.TrivialSource do 2 | @moduledoc """ 3 | This is the most basic source. It does nothing, but is used in tests. 4 | """ 5 | 6 | use Bunch 7 | use Membrane.Source 8 | 9 | def_output_pad :output, flow_control: :manual, accepted_format: _any 10 | end 11 | -------------------------------------------------------------------------------- /test/support/log_metadata_test/pipeline.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Support.LogMetadataTest.Pipeline do 2 | @moduledoc """ 3 | Pipeline used to test log metadata. 4 | Returns `Membrane.ChildrenSpec` containing `:log_metadata`. 5 | """ 6 | use Membrane.Pipeline 7 | 8 | defmodule MetadataNotifyingElement do 9 | @moduledoc """ 10 | Element that notifies the parent with its logger metadata immediately after init. 11 | """ 12 | 13 | use Membrane.Filter 14 | 15 | import Membrane.ChildrenSpec 16 | 17 | def_output_pad :output, 18 | flow_control: :manual, 19 | accepted_format: _any, 20 | availability: :on_request 21 | 22 | def_input_pad :input, 23 | flow_control: :manual, 24 | demand_unit: :buffers, 25 | accepted_format: _any, 26 | availability: :on_request 27 | 28 | @impl true 29 | def handle_init(_ctx, _opts) do 30 | {[notify_parent: Logger.metadata()], %{}} 31 | end 32 | end 33 | 34 | @impl true 35 | def handle_init(_ctx, opts) do 36 | actions = 37 | opts.elements 38 | |> Enum.map(fn {element_name, element_metadata} -> 39 | {:spec, 40 | {child(element_name, MetadataNotifyingElement), log_metadata: [test: element_metadata]}} 41 | end) 42 | 43 | {actions, %{}} 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /test/support/stream_format_mock.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.StreamFormat.Mock do 2 | @moduledoc false 3 | defstruct integer: 42, 4 | string: "mock" 5 | end 6 | -------------------------------------------------------------------------------- /test/support/sync/helper.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Support.Sync.Helper do 2 | @moduledoc false 3 | 4 | @timeout 500 5 | 6 | @spec receive_ticks(non_neg_integer()) :: non_neg_integer() 7 | def receive_ticks(amount \\ 0) do 8 | receive do 9 | :tick -> receive_ticks(amount + 1) 10 | after 11 | @timeout -> amount 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/support/sync/pipeline.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Support.Sync.Pipeline do 2 | @moduledoc false 3 | use Membrane.Pipeline 4 | 5 | import Membrane.ChildrenSpec 6 | 7 | alias Membrane.Testing.{Sink, Source} 8 | 9 | @spec default_spec() :: Membrane.ChildrenSpec.t() 10 | def default_spec() do 11 | demand_generator = fn time, _size -> 12 | Process.sleep(time) 13 | buffer = %Membrane.Buffer{payload: "b"} 14 | {[buffer: {:output, buffer}], time} 15 | end 16 | 17 | children = [ 18 | child(:source_a, %Source{output: ["a"]}), 19 | child(:sink_a, %Sink{}), 20 | child(:source_b, %Source{output: {200, demand_generator}}), 21 | child(:sink_b, %Sink{}) 22 | ] 23 | 24 | links = [ 25 | get_child(:source_a) |> get_child(:sink_a), 26 | get_child(:source_b) |> get_child(:sink_b) 27 | ] 28 | 29 | { 30 | children ++ links, 31 | stream_sync: :sinks 32 | } 33 | end 34 | 35 | @impl true 36 | def handle_init(_ctx, spec) do 37 | {[spec: spec], %{}} 38 | end 39 | 40 | @impl true 41 | def handle_info({:spawn_children, spec}, _ctx, state) do 42 | {[spec: spec], state} 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /test/support/sync/sink.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Support.Sync.Sink do 2 | @moduledoc false 3 | use Membrane.Sink 4 | 5 | def_input_pad :input, flow_control: :manual, accepted_format: _any, demand_unit: :buffers 6 | 7 | def_clock() 8 | end 9 | -------------------------------------------------------------------------------- /test/support/sync/source.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Support.Sync.Source do 2 | @moduledoc false 3 | use Membrane.Source 4 | 5 | def_output_pad :output, flow_control: :manual, accepted_format: _any 6 | 7 | def_options tick_interval: [spec: Membrane.Time.t()], 8 | test_process: [spec: pid()] 9 | 10 | @impl true 11 | def handle_playing(_ctx, %{tick_interval: interval} = state) do 12 | {[notify_parent: :start_timer, start_timer: {:my_timer, interval}], state} 13 | end 14 | 15 | @impl true 16 | def handle_tick(:my_timer, _ctx, state) do 17 | send(state.test_process, :tick) 18 | {[], state} 19 | end 20 | 21 | @impl true 22 | def handle_demand(:output, _size, _unit, _ctx, state), do: {[], state} 23 | end 24 | -------------------------------------------------------------------------------- /test/support/sync/sync_bin.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Support.Sync.SyncBin do 2 | @moduledoc false 3 | use Membrane.Bin 4 | 5 | alias Membrane.Support.Sync 6 | 7 | @impl true 8 | def handle_init(_ctx, _options) do 9 | {[spec: Sync.Pipeline.default_spec()], %{}} 10 | end 11 | 12 | @impl true 13 | def handle_element_start_of_stream(child, _pad, _ctx, state) do 14 | {[notify_parent: {:start_of_stream, child}], state} 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test/support/trivial_pipeline.ex: -------------------------------------------------------------------------------- 1 | defmodule Membrane.Support.TrivialPipeline do 2 | @moduledoc false 3 | use Membrane.Pipeline 4 | end 5 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | # needed for testing distribution features 2 | System.cmd("epmd", ["-daemon"]) 3 | Node.start(:"my_node@127.0.0.1", :longnames) 4 | 5 | ExUnit.configure(formatters: [ExUnit.CLIFormatter, JUnitFormatter]) 6 | ExUnit.start(exclude: [:long_running], capture_log: true, assert_receive_timeout: 500) 7 | --------------------------------------------------------------------------------