├── .editorconfig ├── .github └── workflows │ └── gh-pages.yml ├── .gitignore ├── .gitmodules ├── .prettierrc.js ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── ava.config.js ├── build.sh ├── docs ├── development.md ├── h264.md ├── screenshot.png └── todo.md ├── package-lock.json ├── package.json ├── src ├── assets │ └── styles │ │ └── global.css ├── binding │ ├── binding.cpp │ ├── reader.cpp │ ├── reader.h │ ├── types.h │ └── value-array.h ├── components │ ├── About.vue │ ├── App.vue │ ├── HeaderInfo.vue │ ├── HeaderList.vue │ ├── Link.vue │ ├── Loader.vue │ ├── Pagination.vue │ ├── Payload │ │ ├── Bytes.vue │ │ ├── DecRefPicMarking.vue │ │ ├── HeaderSVCExtension.vue │ │ ├── HrdParameters.vue │ │ ├── Payload.vue │ │ ├── PayloadAUD.vue │ │ ├── PayloadMissing.vue │ │ ├── PayloadPPS.vue │ │ ├── PayloadPrefixNAL.vue │ │ ├── PayloadSEI.vue │ │ ├── PayloadSPS.vue │ │ ├── PayloadSPSSubset.vue │ │ ├── PayloadSliceHeader.vue │ │ ├── PredWeightTable.vue │ │ ├── RefPicListReordering.vue │ │ ├── ScalingList.vue │ │ └── index.js │ ├── Shortcuts.vue │ └── Table │ │ ├── Cell.vue │ │ ├── CollapseRows.vue │ │ ├── HeaderCell.vue │ │ ├── HeaderRow.vue │ │ ├── Row.vue │ │ ├── Table.vue │ │ ├── TodoRow.vue │ │ └── index.js ├── helpers │ └── h264Helper.js ├── index.html ├── lib │ ├── EventEmitter.js │ ├── EventEmitter.test.js │ ├── FileChunkReader.js │ ├── FileChunkReader.test.js │ ├── FileReadStream.js │ ├── FileReadStream.test.js │ ├── H264BitstreamBinding.js │ ├── H264BitstreamConstants.js │ ├── H264BitstreamFile.js │ ├── H264BitstreamHeaderStream.js │ ├── H264BitstreamHeaderStream.test.js │ ├── H264BitstreamParser.js │ ├── H264BitstreamParser.test.js │ ├── KeyboardListener.js │ └── index.js ├── main.js └── utils │ ├── arrayUtils.js │ ├── arrayUtils.test.js │ └── threadUtils.js ├── test └── helpers │ └── setupBrowserEnv.js └── webpack.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [Makefile] 12 | indent_style = tab 13 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/marketplace/actions/deploy-to-github-pages 2 | 3 | name: Build and Deploy 4 | on: [push] 5 | jobs: 6 | build-and-deploy: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v2 11 | with: 12 | submodules: 'true' 13 | 14 | - name: Docker 15 | run: | 16 | mkdir -p dist/ 17 | docker build -t h264bitstream-wasm . 18 | docker create -ti --name h264bitstream-wasm-container h264bitstream-wasm 19 | docker cp h264bitstream-wasm-container:/build/dist/ . 20 | docker rm -fv h264bitstream-wasm-container 21 | 22 | - name: Install and Build 23 | run: | 24 | npm ci 25 | npm run build 26 | 27 | - name: Deploy 28 | uses: JamesIves/github-pages-deploy-action@4.2.1 29 | with: 30 | branch: gh-pages # The branch the action should deploy to. 31 | folder: dist # The folder the action should deploy. 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Package manager 2 | node_modules/ 3 | 4 | # Build 5 | dist/ 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "h264bitstream"] 2 | path = h264bitstream 3 | url = https://github.com/aizvorski/h264bitstream.git 4 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: 'always', 3 | singleQuote: true, 4 | trailingComma: 'all', 5 | }; 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM trzeci/emscripten 2 | 3 | RUN apt-get update && apt-get install -y autoconf libtool 4 | 5 | COPY ./h264bitstream/ /build/h264bitstream/ 6 | 7 | # Comment the line so that it won't build binaries. We only need shared library. 8 | # Otherwise, Emscripten won't be able to complete the build, as the target is 9 | # not a JS target, so it will ignore shared library. But binaries use shared 10 | # libraries, therefore the whole build will fail. 11 | RUN sed -i 's/bin_PROGRAMS/#bin_PROGRAMS/' /build/h264bitstream/Makefile.am 12 | 13 | COPY ./src/binding/ /build/src/binding/ 14 | COPY ./Makefile /build/Makefile 15 | 16 | WORKDIR /build 17 | 18 | RUN make 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Michael Radionov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | dist/h264bitstream.js: \ 2 | src/binding/binding.cpp \ 3 | src/binding/reader.cpp \ 4 | h264bitstream/.libs/libh264bitstream.so.0.0.0 5 | mkdir dist 6 | emcc \ 7 | --bind \ 8 | -O2 \ 9 | -s ASSERTIONS=1 \ 10 | -s ALLOW_MEMORY_GROWTH=1 \ 11 | -s EXTRA_EXPORTED_RUNTIME_METHODS='["setValue"]' \ 12 | -Ih264bitstream/ \ 13 | -Isrc/wrapper/ \ 14 | -o $@ \ 15 | $^ 16 | 17 | h264bitstream/.libs/libh264bitstream.so.0.0.0: 18 | cd h264bitstream && \ 19 | autoreconf -i && \ 20 | emconfigure ./configure && \ 21 | emmake make CFLAGS=-DHAVE_SEI 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # h264-bitstream-viewer [![Build status](https://travis-ci.com/mradionov/h264-bitstream-viewer.svg?branch=master)](https://travis-ci.com/mradionov/h264-bitstream-viewer) 2 | 3 | > Web UI on top of [h264bitstream][h264bitstream], inspired by [H264Naked][h264naked], to display information about NAL units of H264 bitstream. 4 | 5 | ### [Web application available here][web] 6 | 7 | ![Screenshot](./docs/screenshot.png) 8 | 9 | ## Documentation 10 | 11 | - [Development](./docs/development.md) 12 | - [H264](./docs/h264.md) 13 | - [TODO](./docs/todo.md) 14 | 15 | [h264bitstream]: https://github.com/aizvorski/h264bitstream 16 | [h264naked]: https://github.com/shi-yan/H264Naked 17 | [web]: https://mradionov.github.io/h264-bitstream-viewer/ 18 | 19 | ## License 20 | 21 | [MIT](LICENSE) 22 | -------------------------------------------------------------------------------- /ava.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | require: [ 3 | // https://github.com/avajs/ava/blob/master/docs/recipes/es-modules.md 4 | 'esm', 5 | 6 | // https://github.com/avajs/ava/blob/master/docs/recipes/browser-testing.md 7 | './test/helpers/setupBrowserEnv.js', 8 | ], 9 | }; 10 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | mkdir -p dist 2 | docker build -t mradionov/h264bitstream-wasm . 3 | docker create -ti --name h264bitstream-wasm-container mradionov/h264bitstream-wasm 4 | docker cp h264bitstream-wasm-container:/build/dist/ . 5 | docker rm -fv h264bitstream-wasm-container 6 | -------------------------------------------------------------------------------- /docs/development.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | ## Prerequisites 4 | 5 | - Docker - for building WASM version of [h264bitstream][h264bitstream] 6 | - Node.js, npm - for building Frontend Application 7 | 8 | ## Dependencies 9 | 10 | Fetch [h264bitstream][h264bitstream] as a submodule: 11 | 12 | ```bash 13 | git submodule update --init --recursive 14 | ``` 15 | 16 | Fetch NPM dependencies: 17 | 18 | ```bash 19 | npm install 20 | ``` 21 | 22 | ## Build 23 | 24 | Build WASM: 25 | 26 | ```bash 27 | ./build.sh 28 | ``` 29 | 30 | Build Frontend Application: 31 | 32 | ```bash 33 | npm run build 34 | npm run build:watch 35 | ``` 36 | 37 | ## Develop 38 | 39 | Serve `dist/` folder using any web-server and open `index.html` in browser. 40 | 41 | [h264bitstream]: https://github.com/aizvorski/h264bitstream 42 | 43 | ## Test 44 | 45 | ```bash 46 | npm run test 47 | npm run test:watch 48 | ``` 49 | 50 | [h264bitstream]: https://github.com/aizvorski/h264bitstream 51 | -------------------------------------------------------------------------------- /docs/h264.md: -------------------------------------------------------------------------------- 1 | ### H264 2 | 3 | How to extract h264 bistream from a video using FFMPEG: 4 | 5 | ```bash 6 | ffmpeg -i test.mp4 -vcodec copy -an -bsf:v h264_mp4toannexb test.h264 7 | ``` 8 | -------------------------------------------------------------------------------- /docs/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mradionov/h264-bitstream-viewer/42518ae1c98c62e7b5d7ccfaf7ecdec94fed3292/docs/screenshot.png -------------------------------------------------------------------------------- /docs/todo.md: -------------------------------------------------------------------------------- 1 | ### TODO 2 | 3 | - highlight rows in different colors depending on unit type 4 | - expand/collapse nested payload structures 5 | - improve navigation 6 | - filter by unit type 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "h264-bitstream-viewer", 3 | "version": "0.1.0", 4 | "private": true, 5 | "description": "H264 bistream viewer", 6 | "author": "Michael Radionov (https://github.com/mradionov)", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/mradionov/h264-bitstream-viewer.git" 11 | }, 12 | "scripts": { 13 | "build": "webpack", 14 | "build:watch": "npm run build -- --watch", 15 | "start": "npm run build:watch", 16 | "test": "ava --verbose", 17 | "test:watch": "npm run test -- --watch" 18 | }, 19 | "dependencies": { 20 | "css-loader": "^2.1.1", 21 | "html-webpack-plugin": "^4.5.0", 22 | "keen-ui": "^1.1.2", 23 | "style-loader": "^2.0.0", 24 | "vue": "^2.6.10", 25 | "vue-loader": "^15.9.3", 26 | "vue-style-loader": "^4.1.2", 27 | "vue-template-compiler": "^2.6.10", 28 | "webpack": "^5.2.0", 29 | "webpack-cli": "^4.1.0" 30 | }, 31 | "devDependencies": { 32 | "ava": "^3.13.0", 33 | "browser-env": "^3.2.6", 34 | "esm": "^3.2.22", 35 | "ninos": "^2.0.2", 36 | "prettier": "^1.17.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/assets/styles/global.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 4 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica, Arial, sans-serif; 5 | font-size: 95%; 6 | height: 100%; 7 | margin: 0; 8 | padding: 0; 9 | } 10 | 11 | body { 12 | display: flex; 13 | flex-direction: column; 14 | } 15 | 16 | *, 17 | *::before, 18 | *::after { 19 | box-sizing: border-box; 20 | } 21 | -------------------------------------------------------------------------------- /src/binding/binding.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "reader.h" 10 | #include "types.h" 11 | #include "value-array.h" 12 | 13 | EMSCRIPTEN_BINDINGS(H264Bitstream) { 14 | 15 | emscripten::class_("Reader") 16 | .constructor<>() 17 | .function("readPPS", &Reader::readPPS) 18 | .function("readSPS", &Reader::readSPS) 19 | .function("readSEI", &Reader::readSEI) 20 | .function("readAUD", &Reader::readAUD) 21 | .function("readSliceHeader", &Reader::readSliceHeader) 22 | .function("readSPSSubset", &Reader::readSPSSubset) 23 | .function("readPrefixNAL", &Reader::readPrefixNAL) 24 | .function("readNALHeaderSVCEXT", &Reader::readNALHeaderSVCEXT) 25 | ; 26 | 27 | initValueArray("array_int_2"); 28 | initValueArray("array_int_6"); 29 | initValueArray("array_int_8"); 30 | initValueArray("array_int_12"); 31 | initValueArray("array_int_32"); 32 | initValueArray("array_int_64"); 33 | initValueArray("array_int_96"); 34 | initValueArray("array_int_128"); 35 | initValueArray("array_int_256"); 36 | initValueArray("array_int_384"); 37 | 38 | emscripten::register_vector("vector_bind_sei"); 39 | emscripten::register_vector("vector_uint8"); 40 | 41 | emscripten::value_object("pps_t") 42 | .field("pic_parameter_set_id", &pps_t::pic_parameter_set_id) 43 | .field("seq_parameter_set_id", &pps_t::seq_parameter_set_id) 44 | .field("entropy_coding_mode_flag", &pps_t::entropy_coding_mode_flag) 45 | .field("pic_order_present_flag", &pps_t::pic_order_present_flag) 46 | .field("num_slice_groups_minus1", &pps_t::num_slice_groups_minus1) 47 | .field("slice_group_map_type", &pps_t::slice_group_map_type) 48 | .field("run_length_minus1", &pps_t::run_length_minus1) 49 | .field("top_left", &pps_t::top_left) 50 | .field("bottom_right", &pps_t::bottom_right) 51 | .field( 52 | "slice_group_change_direction_flag", 53 | &pps_t::slice_group_change_direction_flag 54 | ) 55 | .field( 56 | "slice_group_change_rate_minus1", 57 | &pps_t::slice_group_change_rate_minus1 58 | ) 59 | .field("pic_size_in_map_units_minus1", &pps_t::pic_size_in_map_units_minus1) 60 | .field("slice_group_id", &pps_t::slice_group_id) 61 | .field("num_ref_idx_l0_active_minus1", &pps_t::num_ref_idx_l0_active_minus1) 62 | .field("num_ref_idx_l1_active_minus1", &pps_t::num_ref_idx_l1_active_minus1) 63 | .field("weighted_pred_flag", &pps_t::weighted_pred_flag) 64 | .field("weighted_bipred_idc", &pps_t::weighted_bipred_idc) 65 | .field("pic_init_qp_minus26", &pps_t::pic_init_qp_minus26) 66 | .field("pic_init_qs_minus26", &pps_t::pic_init_qs_minus26) 67 | .field("chroma_qp_index_offset", &pps_t::chroma_qp_index_offset) 68 | .field( 69 | "deblocking_filter_control_present_flag", 70 | &pps_t::deblocking_filter_control_present_flag 71 | ) 72 | .field("constrained_intra_pred_flag", &pps_t::constrained_intra_pred_flag) 73 | .field( 74 | "redundant_pic_cnt_present_flag", 75 | &pps_t::redundant_pic_cnt_present_flag 76 | ) 77 | .field("transform_8x8_mode_flag", &pps_t::transform_8x8_mode_flag) 78 | .field( 79 | "pic_scaling_matrix_present_flag", 80 | &pps_t::pic_scaling_matrix_present_flag 81 | ) 82 | .field( 83 | "pic_scaling_list_present_flag", 84 | &pps_t::pic_scaling_list_present_flag 85 | ) 86 | .field( 87 | "ScalingList4x4", 88 | &readArray2d, 89 | &writeArray2d 90 | ) 91 | .field( 92 | "UseDefaultScalingMatrix4x4Flag", 93 | &pps_t::UseDefaultScalingMatrix4x4Flag 94 | ) 95 | .field( 96 | "ScalingList8x8", 97 | &readArray2d, 98 | &writeArray2d 99 | ) 100 | .field( 101 | "UseDefaultScalingMatrix8x8Flag", 102 | &pps_t::UseDefaultScalingMatrix8x8Flag 103 | ) 104 | .field( 105 | "second_chroma_qp_index_offset", 106 | &pps_t::second_chroma_qp_index_offset 107 | ) 108 | ; 109 | 110 | emscripten::value_object("sps_t") 111 | .field("profile_idc", &sps_t::profile_idc) 112 | .field("constraint_set0_flag", &sps_t::constraint_set0_flag) 113 | .field("constraint_set1_flag", &sps_t::constraint_set1_flag) 114 | .field("constraint_set2_flag", &sps_t::constraint_set2_flag) 115 | .field("constraint_set3_flag", &sps_t::constraint_set3_flag) 116 | .field("constraint_set4_flag", &sps_t::constraint_set4_flag) 117 | .field("constraint_set5_flag", &sps_t::constraint_set5_flag) 118 | .field("reserved_zero_2bits", &sps_t::reserved_zero_2bits) 119 | .field("level_idc", &sps_t::level_idc) 120 | .field("seq_parameter_set_id", &sps_t::seq_parameter_set_id) 121 | .field("chroma_format_idc", &sps_t::chroma_format_idc) 122 | .field("residual_colour_transform_flag", &sps_t::residual_colour_transform_flag) 123 | .field("bit_depth_luma_minus8", &sps_t::bit_depth_luma_minus8) 124 | .field("bit_depth_chroma_minus8", &sps_t::bit_depth_chroma_minus8) 125 | .field("qpprime_y_zero_transform_bypass_flag", &sps_t::qpprime_y_zero_transform_bypass_flag) 126 | .field("seq_scaling_matrix_present_flag", &sps_t::seq_scaling_matrix_present_flag) 127 | .field("seq_scaling_list_present_flag", &sps_t::seq_scaling_list_present_flag) 128 | .field( 129 | "ScalingList4x4", 130 | &readArray2d, 131 | &writeArray2d 132 | ) 133 | .field("UseDefaultScalingMatrix4x4Flag", &sps_t::UseDefaultScalingMatrix4x4Flag) 134 | .field( 135 | "ScalingList8x8", 136 | &readArray2d, 137 | &writeArray2d 138 | ) 139 | .field("UseDefaultScalingMatrix8x8Flag", &sps_t::UseDefaultScalingMatrix8x8Flag) 140 | .field("log2_max_frame_num_minus4", &sps_t::log2_max_frame_num_minus4) 141 | .field("pic_order_cnt_type", &sps_t::pic_order_cnt_type) 142 | .field("log2_max_pic_order_cnt_lsb_minus4", &sps_t::log2_max_pic_order_cnt_lsb_minus4) 143 | /* 144 | int delta_pic_order_always_zero_flag; 145 | int offset_for_non_ref_pic; 146 | int offset_for_top_to_bottom_field; 147 | int num_ref_frames_in_pic_order_cnt_cycle; 148 | int offset_for_ref_frame[256]; 149 | */ 150 | .field("num_ref_frames", &sps_t::num_ref_frames) 151 | .field("gaps_in_frame_num_value_allowed_flag", &sps_t::gaps_in_frame_num_value_allowed_flag) 152 | .field("pic_width_in_mbs_minus1", &sps_t::pic_width_in_mbs_minus1) 153 | .field("pic_height_in_map_units_minus1", &sps_t::pic_height_in_map_units_minus1) 154 | .field("frame_mbs_only_flag", &sps_t::frame_mbs_only_flag) 155 | .field("mb_adaptive_frame_field_flag", &sps_t::mb_adaptive_frame_field_flag) 156 | .field("direct_8x8_inference_flag", &sps_t::direct_8x8_inference_flag) 157 | .field("frame_cropping_flag", &sps_t::frame_cropping_flag) 158 | .field("frame_crop_left_offset", &sps_t::frame_crop_left_offset) 159 | .field("frame_crop_right_offset", &sps_t::frame_crop_right_offset) 160 | .field("frame_crop_top_offset", &sps_t::frame_crop_top_offset) 161 | .field("frame_crop_bottom_offset", &sps_t::frame_crop_bottom_offset) 162 | .field("vui_parameters_present_flag", &sps_t::vui_parameters_present_flag) 163 | .field("vui", &sps_t::vui) 164 | .field("hrd_nal", &sps_t::hrd_nal) 165 | .field("hrd_vcl", &sps_t::hrd_vcl) 166 | ; 167 | 168 | using sps_vui_t = decltype(sps_t::vui); 169 | 170 | emscripten::value_object("sps_vui_t") 171 | .field("aspect_ratio_info_present_flag", &sps_vui_t::aspect_ratio_info_present_flag) 172 | .field("aspect_ratio_idc", &sps_vui_t::aspect_ratio_idc) 173 | .field("sar_width", &sps_vui_t::sar_width) 174 | .field("sar_height", &sps_vui_t::sar_height) 175 | .field("overscan_info_present_flag", &sps_vui_t::overscan_info_present_flag) 176 | .field("overscan_appropriate_flag", &sps_vui_t::overscan_appropriate_flag) 177 | .field("video_signal_type_present_flag", &sps_vui_t::video_signal_type_present_flag) 178 | .field("video_format", &sps_vui_t::video_format) 179 | .field("video_full_range_flag", &sps_vui_t::video_full_range_flag) 180 | .field("colour_description_present_flag", &sps_vui_t::colour_description_present_flag) 181 | .field("colour_primaries", &sps_vui_t::colour_primaries) 182 | .field("transfer_characteristics", &sps_vui_t::transfer_characteristics) 183 | .field("matrix_coefficients", &sps_vui_t::matrix_coefficients) 184 | .field("chroma_loc_info_present_flag", &sps_vui_t::chroma_loc_info_present_flag) 185 | .field("chroma_sample_loc_type_top_field", &sps_vui_t::chroma_sample_loc_type_top_field) 186 | .field("chroma_sample_loc_type_bottom_field", &sps_vui_t::chroma_sample_loc_type_bottom_field) 187 | .field("timing_info_present_flag", &sps_vui_t::timing_info_present_flag) 188 | .field("num_units_in_tick", &sps_vui_t::num_units_in_tick) 189 | .field("time_scale", &sps_vui_t::time_scale) 190 | .field("fixed_frame_rate_flag", &sps_vui_t::fixed_frame_rate_flag) 191 | .field("nal_hrd_parameters_present_flag", &sps_vui_t::nal_hrd_parameters_present_flag) 192 | .field("vcl_hrd_parameters_present_flag", &sps_vui_t::vcl_hrd_parameters_present_flag) 193 | .field("low_delay_hrd_flag", &sps_vui_t::low_delay_hrd_flag) 194 | .field("pic_struct_present_flag", &sps_vui_t::pic_struct_present_flag) 195 | .field("bitstream_restriction_flag", &sps_vui_t::bitstream_restriction_flag) 196 | .field("motion_vectors_over_pic_boundaries_flag", &sps_vui_t::motion_vectors_over_pic_boundaries_flag) 197 | .field("max_bytes_per_pic_denom", &sps_vui_t::max_bytes_per_pic_denom) 198 | .field("max_bits_per_mb_denom", &sps_vui_t::max_bits_per_mb_denom) 199 | .field("log2_max_mv_length_horizontal", &sps_vui_t::log2_max_mv_length_horizontal) 200 | .field("log2_max_mv_length_vertical", &sps_vui_t::log2_max_mv_length_vertical) 201 | .field("num_reorder_frames", &sps_vui_t::num_reorder_frames) 202 | .field("max_dec_frame_buffering", &sps_vui_t::max_dec_frame_buffering) 203 | ; 204 | 205 | emscripten::value_object("hrd_t") 206 | .field("cpb_cnt_minus1", &hrd_t::cpb_cnt_minus1) 207 | .field("bit_rate_scale", &hrd_t::bit_rate_scale) 208 | .field("cpb_size_scale", &hrd_t::cpb_size_scale) 209 | .field("bit_rate_value_minus1", &hrd_t::bit_rate_value_minus1) 210 | .field("cpb_size_value_minus1", &hrd_t::cpb_size_value_minus1) 211 | .field("cbr_flag", &hrd_t::cbr_flag) 212 | .field("initial_cpb_removal_delay_length_minus1", &hrd_t::initial_cpb_removal_delay_length_minus1) 213 | .field("cpb_removal_delay_length_minus1", &hrd_t::cpb_removal_delay_length_minus1) 214 | .field("dpb_output_delay_length_minus1", &hrd_t::dpb_output_delay_length_minus1) 215 | .field("time_offset_length", &hrd_t::time_offset_length) 216 | ; 217 | 218 | emscripten::value_object("slice_header_t") 219 | .field("first_mb_in_slice", &slice_header_t::first_mb_in_slice) 220 | .field("slice_type", &slice_header_t::slice_type) 221 | .field("pic_parameter_set_id", &slice_header_t::pic_parameter_set_id) 222 | .field("colour_plane_id", &slice_header_t::colour_plane_id) 223 | .field("frame_num", &slice_header_t::frame_num) 224 | .field("field_pic_flag", &slice_header_t::field_pic_flag) 225 | .field("bottom_field_flag", &slice_header_t::bottom_field_flag) 226 | .field("idr_pic_id", &slice_header_t::idr_pic_id) 227 | .field("pic_order_cnt_lsb", &slice_header_t::pic_order_cnt_lsb) 228 | .field("delta_pic_order_cnt_bottom", &slice_header_t::delta_pic_order_cnt_bottom) 229 | .field("delta_pic_order_cnt", &slice_header_t::delta_pic_order_cnt) 230 | .field("redundant_pic_cnt", &slice_header_t::redundant_pic_cnt) 231 | .field("direct_spatial_mv_pred_flag", &slice_header_t::direct_spatial_mv_pred_flag) 232 | .field("num_ref_idx_active_override_flag", &slice_header_t::num_ref_idx_active_override_flag) 233 | .field("num_ref_idx_l0_active_minus1", &slice_header_t::num_ref_idx_l0_active_minus1) 234 | .field("num_ref_idx_l1_active_minus1", &slice_header_t::num_ref_idx_l1_active_minus1) 235 | .field("cabac_init_idc", &slice_header_t::cabac_init_idc) 236 | .field("slice_qp_delta", &slice_header_t::slice_qp_delta) 237 | .field("sp_for_switch_flag", &slice_header_t::sp_for_switch_flag) 238 | .field("slice_qs_delta", &slice_header_t::slice_qs_delta) 239 | .field("disable_deblocking_filter_idc", &slice_header_t::disable_deblocking_filter_idc) 240 | .field("slice_alpha_c0_offset_div2", &slice_header_t::slice_alpha_c0_offset_div2) 241 | .field("slice_beta_offset_div2", &slice_header_t::slice_beta_offset_div2) 242 | .field("slice_group_change_cycle", &slice_header_t::slice_group_change_cycle) 243 | .field("rplr", &slice_header_t::rplr) 244 | .field("drpm", &slice_header_t::drpm) 245 | .field("pwt", &slice_header_t::pwt) 246 | ; 247 | 248 | using rplr_t = decltype(slice_header_t::rplr); 249 | 250 | emscripten::value_object("rplr_t") 251 | .field("ref_pic_list_reordering_flag_l0", &rplr_t::ref_pic_list_reordering_flag_l0) 252 | .field("reorder_l0", &rplr_t::reorder_l0) 253 | .field("ref_pic_list_reordering_flag_l1", &rplr_t::ref_pic_list_reordering_flag_l1) 254 | .field("reorder_l1", &rplr_t::reorder_l1) 255 | ; 256 | 257 | using rplr_reorder_l0_t = decltype(rplr_t::reorder_l0); 258 | 259 | emscripten::value_object("rplr_reorder_l0_t") 260 | .field("reordering_of_pic_nums_idc", &rplr_reorder_l0_t::reordering_of_pic_nums_idc) 261 | .field("abs_diff_pic_num_minus1", &rplr_reorder_l0_t::abs_diff_pic_num_minus1) 262 | .field("long_term_pic_num", &rplr_reorder_l0_t::long_term_pic_num) 263 | ; 264 | 265 | using rplr_reorder_l1_t = decltype(rplr_t::reorder_l1); 266 | 267 | emscripten::value_object("rplr_reorder_l1_t") 268 | .field("reordering_of_pic_nums_idc", &rplr_reorder_l1_t::reordering_of_pic_nums_idc) 269 | .field("abs_diff_pic_num_minus1", &rplr_reorder_l1_t::abs_diff_pic_num_minus1) 270 | .field("long_term_pic_num", &rplr_reorder_l1_t::long_term_pic_num) 271 | ; 272 | 273 | using drpm_t = decltype(slice_header_t::drpm); 274 | 275 | emscripten::value_object("drpm_t") 276 | .field("no_output_of_prior_pics_flag", &drpm_t::no_output_of_prior_pics_flag) 277 | .field("long_term_reference_flag", &drpm_t::long_term_reference_flag) 278 | .field("adaptive_ref_pic_marking_mode_flag", &drpm_t::adaptive_ref_pic_marking_mode_flag) 279 | .field("memory_management_control_operation", &drpm_t::memory_management_control_operation) 280 | .field("difference_of_pic_nums_minus1", &drpm_t::difference_of_pic_nums_minus1) 281 | .field("long_term_pic_num", &drpm_t::long_term_pic_num) 282 | .field("long_term_frame_idx", &drpm_t::long_term_frame_idx) 283 | .field("max_long_term_frame_idx_plus1", &drpm_t::max_long_term_frame_idx_plus1) 284 | ; 285 | 286 | using pwt_t = decltype(slice_header_t::pwt); 287 | 288 | emscripten::value_object("pwt_t") 289 | .field("luma_log2_weight_denom", &pwt_t::luma_log2_weight_denom) 290 | .field("chroma_log2_weight_denom", &pwt_t::chroma_log2_weight_denom) 291 | .field("luma_weight_l0_flag", &pwt_t::luma_weight_l0_flag) 292 | .field("luma_weight_l0", &pwt_t::luma_weight_l0) 293 | .field("luma_offset_l0", &pwt_t::luma_offset_l0) 294 | .field("chroma_weight_l0_flag", &pwt_t::chroma_weight_l0_flag) 295 | .field( 296 | "chroma_weight_l0", 297 | &readArray2d, 298 | &writeArray2d 299 | ) 300 | .field( 301 | "chroma_offset_l0", 302 | &readArray2d, 303 | &writeArray2d 304 | ) 305 | .field("luma_weight_l1_flag", &pwt_t::luma_weight_l1_flag) 306 | .field("luma_weight_l1", &pwt_t::luma_weight_l1) 307 | .field("luma_offset_l1", &pwt_t::luma_offset_l1) 308 | .field("chroma_weight_l1_flag", &pwt_t::chroma_weight_l1_flag) 309 | .field( 310 | "chroma_weight_l1", 311 | &readArray2d, 312 | &writeArray2d 313 | ) 314 | .field( 315 | "chroma_offset_l1", 316 | &readArray2d, 317 | &writeArray2d 318 | ) 319 | ; 320 | 321 | emscripten::value_object("bind_sei_t") 322 | .field("payloadType", &bind_sei_t::payloadType) 323 | .field("payloadSize", &bind_sei_t::payloadSize) 324 | .field("data", &bind_sei_t::data) 325 | ; 326 | 327 | emscripten::value_object("bind_sps_subset_t") 328 | .field("additional_extension2_flag", &bind_sps_subset_t::additional_extension2_flag) 329 | .field("sps", &bind_sps_subset_t::sps) 330 | .field("sps_svc_ext", &bind_sps_subset_t::sps_svc_ext) 331 | ; 332 | 333 | emscripten::value_object("sps_svc_ext_t") 334 | .field("inter_layer_deblocking_filter_control_present_flag", &sps_svc_ext_t::inter_layer_deblocking_filter_control_present_flag) 335 | .field("extended_spatial_scalability_idc", &sps_svc_ext_t::extended_spatial_scalability_idc) 336 | .field("chroma_phase_x_plus1_flag", &sps_svc_ext_t::chroma_phase_x_plus1_flag) 337 | .field("chroma_phase_y_plus1", &sps_svc_ext_t::chroma_phase_y_plus1) 338 | .field("seq_ref_layer_chroma_phase_x_plus1_flag", &sps_svc_ext_t::seq_ref_layer_chroma_phase_x_plus1_flag) 339 | .field("seq_ref_layer_chroma_phase_y_plus1", &sps_svc_ext_t::seq_ref_layer_chroma_phase_y_plus1) 340 | .field("seq_scaled_ref_layer_left_offset", &sps_svc_ext_t::seq_scaled_ref_layer_left_offset) 341 | .field("seq_scaled_ref_layer_top_offset", &sps_svc_ext_t::seq_scaled_ref_layer_top_offset) 342 | .field("seq_scaled_ref_layer_right_offset", &sps_svc_ext_t::seq_scaled_ref_layer_right_offset) 343 | .field("seq_scaled_ref_layer_bottom_offset", &sps_svc_ext_t::seq_scaled_ref_layer_bottom_offset) 344 | .field("seq_tcoeff_level_prediction_flag", &sps_svc_ext_t::seq_tcoeff_level_prediction_flag) 345 | .field("adaptive_tcoeff_level_prediction_flag", &sps_svc_ext_t::adaptive_tcoeff_level_prediction_flag) 346 | .field("slice_header_restriction_flag", &sps_svc_ext_t::slice_header_restriction_flag) 347 | .field("svc_vui_parameters_present_flag", &sps_svc_ext_t::svc_vui_parameters_present_flag) 348 | ; 349 | 350 | emscripten::value_object("prefix_nal_svc_t") 351 | .field("store_ref_base_pic_flag", &prefix_nal_svc_t::store_ref_base_pic_flag) 352 | .field("additional_prefix_nal_unit_extension_flag", &prefix_nal_svc_t::additional_prefix_nal_unit_extension_flag) 353 | .field("additional_prefix_nal_unit_extension_data_flag", &prefix_nal_svc_t::additional_prefix_nal_unit_extension_data_flag) 354 | .field("adaptive_ref_base_pic_marking_mode_flag", &prefix_nal_svc_t::adaptive_ref_base_pic_marking_mode_flag) 355 | .field("memory_management_base_control_operation", &prefix_nal_svc_t::memory_management_base_control_operation) 356 | .field("difference_of_base_pic_nums_minus1", &prefix_nal_svc_t::difference_of_base_pic_nums_minus1) 357 | .field("long_term_base_pic_num", &prefix_nal_svc_t::long_term_base_pic_num) 358 | ; 359 | 360 | emscripten::value_object("nal_svc_ext_t") 361 | .field("idr_flag", &nal_svc_ext_t::idr_flag) 362 | .field("priority_id", &nal_svc_ext_t::priority_id) 363 | .field("no_inter_layer_pred_flag", &nal_svc_ext_t::no_inter_layer_pred_flag) 364 | .field("dependency_id", &nal_svc_ext_t::dependency_id) 365 | .field("quality_id", &nal_svc_ext_t::quality_id) 366 | .field("temporal_id", &nal_svc_ext_t::temporal_id) 367 | .field("use_ref_base_pic_flag", &nal_svc_ext_t::use_ref_base_pic_flag) 368 | .field("discardable_flag", &nal_svc_ext_t::discardable_flag) 369 | .field("output_flag", &nal_svc_ext_t::output_flag) 370 | .field("reserved_three_2bits", &nal_svc_ext_t::reserved_three_2bits) 371 | ; 372 | 373 | emscripten::value_object("aud_t") 374 | .field("primary_pic_type", &aud_t::primary_pic_type) 375 | ; 376 | }; 377 | -------------------------------------------------------------------------------- /src/binding/reader.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | 6 | #include "reader.h" 7 | #include "types.h" 8 | 9 | Reader::Reader() { 10 | m_h264_stream = h264_new(); 11 | } 12 | 13 | Reader::~Reader() { 14 | h264_free(m_h264_stream); 15 | } 16 | 17 | pps_t Reader::readPPS(uintptr_t input, int size) { 18 | uint8_t* data = reinterpret_cast(input); 19 | 20 | read_nal_unit(m_h264_stream, data, size); 21 | 22 | pps_t pps = *(m_h264_stream->pps); 23 | 24 | return pps; 25 | } 26 | 27 | sps_t Reader::readSPS(uintptr_t input, int size) { 28 | uint8_t* data = reinterpret_cast(input); 29 | 30 | read_nal_unit(m_h264_stream, data, size); 31 | 32 | sps_t sps = *(m_h264_stream->sps); 33 | 34 | return sps; 35 | } 36 | 37 | slice_header_t Reader::readSliceHeader(uintptr_t input, int size) { 38 | uint8_t* data = reinterpret_cast(input); 39 | 40 | read_nal_unit(m_h264_stream, data, size); 41 | 42 | slice_header_t sh = *(m_h264_stream->sh); 43 | 44 | return sh; 45 | } 46 | 47 | std::vector Reader::readSEI(uintptr_t input, int size) { 48 | uint8_t* data = reinterpret_cast(input); 49 | 50 | read_nal_unit(m_h264_stream, data, size); 51 | 52 | std::vector result(m_h264_stream->num_seis); 53 | 54 | for (int i = 0; i < m_h264_stream->num_seis; i++) { 55 | sei_t* s = m_h264_stream->seis[i]; 56 | 57 | result[i].payloadType = s->payloadType; 58 | result[i].payloadSize = s->payloadSize; 59 | result[i].data = std::vector(s->data, s->data + s->payloadSize); 60 | } 61 | 62 | return result; 63 | } 64 | 65 | bind_sps_subset_t Reader::readSPSSubset(uintptr_t input, int size) { 66 | uint8_t* data = reinterpret_cast(input); 67 | 68 | read_nal_unit(m_h264_stream, data, size); 69 | 70 | sps_subset_t* sps_subset = m_h264_stream->sps_subset; 71 | 72 | bind_sps_subset_t bind_sps_subset; 73 | bind_sps_subset.sps = *(sps_subset->sps); 74 | bind_sps_subset.additional_extension2_flag = sps_subset->additional_extension2_flag; 75 | bind_sps_subset.sps_svc_ext = *(sps_subset->sps_svc_ext); 76 | 77 | return bind_sps_subset; 78 | } 79 | 80 | prefix_nal_svc_t Reader::readPrefixNAL(uintptr_t input, int size) { 81 | uint8_t* data = reinterpret_cast(input); 82 | 83 | read_nal_unit(m_h264_stream, data, size); 84 | 85 | prefix_nal_svc_t prefix_nal_svc = *(m_h264_stream->nal->prefix_nal_svc); 86 | 87 | return prefix_nal_svc; 88 | } 89 | 90 | nal_svc_ext_t Reader::readNALHeaderSVCEXT(uintptr_t input, int size) { 91 | uint8_t* data = reinterpret_cast(input); 92 | 93 | read_nal_unit(m_h264_stream, data, size); 94 | 95 | nal_svc_ext_t nal_svc_ext = *(m_h264_stream->nal->nal_svc_ext); 96 | 97 | return nal_svc_ext; 98 | } 99 | 100 | aud_t Reader::readAUD(uintptr_t input, int size) { 101 | uint8_t* data = reinterpret_cast(input); 102 | 103 | read_nal_unit(m_h264_stream, data, size); 104 | 105 | aud_t aud = *(m_h264_stream->aud); 106 | 107 | return aud; 108 | } 109 | -------------------------------------------------------------------------------- /src/binding/reader.h: -------------------------------------------------------------------------------- 1 | #ifndef READER_H 2 | #define READER_H 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "types.h" 10 | 11 | class Reader { 12 | public: 13 | Reader(); 14 | ~Reader(); 15 | pps_t readPPS(uintptr_t input, int size); 16 | sps_t readSPS(uintptr_t input, int size); 17 | aud_t readAUD(uintptr_t input, int size); 18 | slice_header_t readSliceHeader(uintptr_t input, int size); 19 | std::vector readSEI(uintptr_t input, int size); 20 | bind_sps_subset_t readSPSSubset(uintptr_t input, int size); 21 | prefix_nal_svc_t readPrefixNAL(uintptr_t input, int size); 22 | nal_svc_ext_t readNALHeaderSVCEXT(uintptr_t input, int size); 23 | private: 24 | h264_stream_t* m_h264_stream; 25 | }; 26 | 27 | #endif 28 | 29 | -------------------------------------------------------------------------------- /src/binding/types.h: -------------------------------------------------------------------------------- 1 | #ifndef TYPES_H 2 | #define TYPES_H 3 | 4 | #include 5 | #include 6 | 7 | // Alternative to sei_t to use vector instead of array. 8 | struct bind_sei_t { 9 | int payloadType; 10 | int payloadSize; 11 | std::vector data; 12 | }; 13 | 14 | struct bind_sps_subset_t { 15 | sps_t sps; 16 | sps_svc_ext_t sps_svc_ext; 17 | bool additional_extension2_flag; 18 | }; 19 | 20 | #endif 21 | -------------------------------------------------------------------------------- /src/binding/value-array.h: -------------------------------------------------------------------------------- 1 | #ifndef VALUE_ARRAY_H 2 | #define VALUE_ARRAY_H 3 | 4 | #include 5 | 6 | #include 7 | 8 | template 9 | class ArrayElementInitializer { 10 | public: 11 | static void init(emscripten::value_array& arr) { 12 | ArrayElementInitializer::init(arr); 13 | arr.element(emscripten::index()); 14 | } 15 | }; 16 | 17 | template 18 | class ArrayElementInitializer { 19 | public: 20 | static void init(emscripten::value_array& arr) {} 21 | }; 22 | 23 | // Use it to bind value_array and all of it's elements. Out of the box, 24 | // you have to call `.element(emscripten::index())` for each index you have, 25 | // which is not nice when you have a lot of elements. The templates above 26 | // recursively initialize all N array elements. 27 | // 28 | // Inspired by: 29 | // http://nnarain.github.io/2019/01/19/GameboyCore-in-the-Web!.html 30 | // https://github.com/nnarain/nnarain.github.io/issues/75 31 | // 32 | // Usage: 33 | // 34 | // initValueArray("my_array_int_10"); 35 | // 36 | template 37 | void initValueArray(const char *name) { 38 | using Arr = std::array; 39 | 40 | emscripten::value_array arr(name); 41 | ArrayElementInitializer::init(arr); 42 | } 43 | 44 | // This is a workaround for Emscripten value_array - not sure how to bind 45 | // 2d-arrays. Expected the following syntax to work but it does not: 46 | // 47 | // emscripten::value_array, 2>>("array_int_2_4"); 48 | // 49 | // Thus instead of binding 2d-arrays, I am using a custom getter+setter on 50 | // a 2d-array member of a struct. Member must be a 2d-array with dimensions NxM. 51 | // Getter will convert 2d-array into a 1d-array and return it as a result. 52 | // 1d-array still has to have a binding of the resulting size. 53 | // 54 | // Usage: 55 | // 56 | // struct Some { 57 | // int foo[2][4]; 58 | // }; 59 | // 60 | // // 2 * 4 = 8 - size of resulting 1d array 61 | // 62 | // emscripten::value_object("Some") 63 | // .field("foo", 64 | // &readArray2d, 65 | // &writeArray2d 66 | // ); 67 | // 68 | // initValueArray("my_array_int_8"); 69 | // 70 | 71 | template< 72 | typename T, 73 | size_t N, 74 | size_t M, 75 | typename Member, 76 | typename Object, 77 | Member Object::*member 78 | > 79 | std::array readArray2d(const Object& obj) { 80 | std::array arr1d; 81 | 82 | for (size_t i = 0; i < N; ++i) { 83 | for (size_t j = 0; j < M; ++j) { 84 | arr1d[i * M + j] = (obj.*member)[i][j]; 85 | } 86 | } 87 | 88 | return arr1d; 89 | } 90 | 91 | template 92 | void writeArray2d(Object& obj, std::array) {} 93 | 94 | #endif 95 | -------------------------------------------------------------------------------- /src/components/About.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 18 | 19 | 29 | -------------------------------------------------------------------------------- /src/components/App.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 279 | 280 | 361 | -------------------------------------------------------------------------------- /src/components/HeaderInfo.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 67 | 68 | 73 | -------------------------------------------------------------------------------- /src/components/HeaderList.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 76 | 77 | 91 | -------------------------------------------------------------------------------- /src/components/Link.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | 13 | 22 | -------------------------------------------------------------------------------- /src/components/Loader.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 20 | 21 | 32 | -------------------------------------------------------------------------------- /src/components/Pagination.vue: -------------------------------------------------------------------------------- 1 | 75 | 76 | 259 | 260 | 302 | -------------------------------------------------------------------------------- /src/components/Payload/Bytes.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 65 | 66 | 73 | -------------------------------------------------------------------------------- /src/components/Payload/DecRefPicMarking.vue: -------------------------------------------------------------------------------- 1 | 63 | 64 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /src/components/Payload/HeaderSVCExtension.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 | 106 | -------------------------------------------------------------------------------- /src/components/Payload/HrdParameters.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 65 | -------------------------------------------------------------------------------- /src/components/Payload/Payload.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 82 | -------------------------------------------------------------------------------- /src/components/Payload/PayloadAUD.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 41 | -------------------------------------------------------------------------------- /src/components/Payload/PayloadMissing.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 23 | -------------------------------------------------------------------------------- /src/components/Payload/PayloadPPS.vue: -------------------------------------------------------------------------------- 1 | 95 | 96 | 123 | -------------------------------------------------------------------------------- /src/components/Payload/PayloadPrefixNAL.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 80 | -------------------------------------------------------------------------------- /src/components/Payload/PayloadSEI.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /src/components/Payload/PayloadSPS.vue: -------------------------------------------------------------------------------- 1 | 338 | 339 | 394 | -------------------------------------------------------------------------------- /src/components/Payload/PayloadSPSSubset.vue: -------------------------------------------------------------------------------- 1 | 105 | 106 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /src/components/Payload/PayloadSliceHeader.vue: -------------------------------------------------------------------------------- 1 | 190 | 191 | 252 | -------------------------------------------------------------------------------- /src/components/Payload/PredWeightTable.vue: -------------------------------------------------------------------------------- 1 | 99 | 100 | 155 | 156 | 157 | -------------------------------------------------------------------------------- /src/components/Payload/RefPicListReordering.vue: -------------------------------------------------------------------------------- 1 | 82 | 83 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /src/components/Payload/ScalingList.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/components/Payload/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Payload'; 2 | -------------------------------------------------------------------------------- /src/components/Shortcuts.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 47 | 48 | 69 | -------------------------------------------------------------------------------- /src/components/Table/Cell.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | 18 | 24 | -------------------------------------------------------------------------------- /src/components/Table/CollapseRows.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 50 | 51 | 63 | -------------------------------------------------------------------------------- /src/components/Table/HeaderCell.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | 18 | 30 | -------------------------------------------------------------------------------- /src/components/Table/HeaderRow.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/components/Table/Row.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/components/Table/Table.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 16 | -------------------------------------------------------------------------------- /src/components/Table/TodoRow.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 42 | 43 | 48 | -------------------------------------------------------------------------------- /src/components/Table/index.js: -------------------------------------------------------------------------------- 1 | export { default as Cell } from './Cell'; 2 | export { default as CollapseRows } from './CollapseRows'; 3 | export { default as HeaderCell } from './HeaderCell'; 4 | export { default as HeaderRow } from './HeaderRow'; 5 | export { default as Row } from './Row'; 6 | export { default as Table } from './Table'; 7 | export { default as TodoRow } from './TodoRow'; 8 | -------------------------------------------------------------------------------- /src/helpers/h264Helper.js: -------------------------------------------------------------------------------- 1 | import { NALU_TYPES, SEI_TYPES, AUD_PRIMARY_PIC_TYPES } from '../lib'; 2 | 3 | const h264Helper = { 4 | naluTypeDisplayed(type) { 5 | switch (type) { 6 | case NALU_TYPES.UNSPECIFIED: 7 | return 'Unspecified'; 8 | case NALU_TYPES.CODED_SLICE_NON_IDR: 9 | return 'Coded slice a of non-IDR picture'; 10 | case NALU_TYPES.CODED_SLICE_DATA_PARTITION_A: 11 | return 'Coded slice data partition A'; 12 | case NALU_TYPES.CODED_SLICE_DATA_PARTITION_B: 13 | return 'Coded slice data partition B'; 14 | case NALU_TYPES.CODED_SLICE_DATA_PARTITION_C: 15 | return 'Coded slice data partition C'; 16 | case NALU_TYPES.CODED_SLICE_IDR: 17 | return 'Coded slice of an IDR picture'; 18 | case NALU_TYPES.SEI: 19 | return 'Supplemental enhancement information (SEI)'; 20 | case NALU_TYPES.SPS: 21 | return 'Sequence parameter set'; 22 | case NALU_TYPES.PPS: 23 | return 'Picture parameter set'; 24 | case NALU_TYPES.AUD: 25 | return 'Access unit delimiter'; 26 | case NALU_TYPES.END_OF_SEQUENCE: 27 | return 'End of sequence'; 28 | case NALU_TYPES.END_OF_STREAM: 29 | return 'End of stream'; 30 | case NALU_TYPES.FILLER: 31 | return 'Filler data'; 32 | case NALU_TYPES.SPS_EXT: 33 | return 'Sequence parameter set extension'; 34 | case NALU_TYPES.PREFIX_NAL: 35 | return 'Prefix NAL unit'; 36 | case NALU_TYPES.SUBSET_SPS: 37 | return 'Subset Sequence parameter set'; 38 | case NALU_TYPES.DPS: 39 | return 'Depth Parameter Set'; 40 | case NALU_TYPES.CODED_SLICE_AUX: 41 | return 'Coded slice of an auxiliary coded picture without partitioning'; 42 | case NALU_TYPES.CODED_SLICE_SVC_EXTENSION: 43 | return 'Coded slice of SVC extension'; 44 | default: 45 | return 'Unknown'; 46 | } 47 | }, 48 | 49 | seiTypeDisplayed(type) { 50 | switch (type) { 51 | case SEI_TYPES.BUFFERING_PERIOD: 52 | return 'Buffering period'; 53 | case SEI_TYPES.PIC_TIMING: 54 | return 'Pic timing'; 55 | case SEI_TYPES.PAN_SCAN_RECT: 56 | return 'Pan scan rect'; 57 | case SEI_TYPES.FILLER_PAYLOAD: 58 | return 'Filler payload'; 59 | case SEI_TYPES.USER_DATA_REGISTERED_ITU_T_T35: 60 | return 'User data registered ITU-T T35'; 61 | case SEI_TYPES.USER_DATA_UNREGISTERED: 62 | return 'User data unregistered'; 63 | case SEI_TYPES.RECOVERY_POINT: 64 | return 'Recovery point'; 65 | case SEI_TYPES.DEC_REF_PIC_MARKING_REPETITION: 66 | return 'Ref pic marking repetition'; 67 | case SEI_TYPES.SPARE_PIC: 68 | return 'Spare pic'; 69 | case SEI_TYPES.SCENE_INFO: 70 | return 'Scene info'; 71 | case SEI_TYPES.SUB_SEQ_INFO: 72 | return 'Sub seq info'; 73 | case SEI_TYPES.SUB_SEQ_LAYER_CHARACTERISTICS: 74 | return 'Sub seq layer characteristics'; 75 | case SEI_TYPES.SUB_SEQ_CHARACTERISTICS: 76 | return 'Sub seq characteristics'; 77 | case SEI_TYPES.FULL_FRAME_FREEZE: 78 | return 'Full frame freeze'; 79 | case SEI_TYPES.FULL_FRAME_FREEZE_RELEASE: 80 | return 'Full frame freeze release'; 81 | case SEI_TYPES.FULL_FRAME_SNAPSHOT: 82 | return 'Full frame snapshot'; 83 | case SEI_TYPES.PROGRESSIVE_REFINEMENT_SEGMENT_START: 84 | return 'Progressive refinement segment start'; 85 | case SEI_TYPES.PROGRESSIVE_REFINEMENT_SEGMENT_END: 86 | return 'Progressive refinement segment end'; 87 | case SEI_TYPES.MOTION_CONSTRAINED_SLICE_GROUP_SET: 88 | return 'Motion constrained slice group set'; 89 | case SEI_TYPES.FILM_GRAIN_CHARACTERISTICS: 90 | return 'Film grain characteristics'; 91 | case SEI_TYPES.DEBLOCKING_FILTER_DISPLAY_PREFERENCE: 92 | return 'Deblocking filter display preference'; 93 | case SEI_TYPES.STEREO_VIDEO_INFO: 94 | return ''; 95 | default: 96 | return 'Unknown'; 97 | } 98 | }, 99 | 100 | audPrimaryPicTypeDisplayed(type) { 101 | switch (type) { 102 | case AUD_PRIMARY_PIC_TYPES.I: 103 | return 'I'; 104 | case AUD_PRIMARY_PIC_TYPES.IP: 105 | return 'I, P'; 106 | case AUD_PRIMARY_PIC_TYPES.IPB: 107 | return 'I, P, B'; 108 | case AUD_PRIMARY_PIC_TYPES.SI: 109 | return 'SI'; 110 | case AUD_PRIMARY_PIC_TYPES.SISP: 111 | return 'SI, SP'; 112 | case AUD_PRIMARY_PIC_TYPES.ISI: 113 | return 'I, SI'; 114 | case AUD_PRIMARY_PIC_TYPES.ISIPSP: 115 | return 'I, SI, P, SP'; 116 | case AUD_PRIMARY_PIC_TYPES.ISIPSPB: 117 | return 'I, SI, P, SP, B'; 118 | default: 119 | return 'Unknown'; 120 | } 121 | }, 122 | }; 123 | 124 | export default h264Helper; 125 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | h264-bitstream-viewer 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/lib/EventEmitter.js: -------------------------------------------------------------------------------- 1 | export class EventEmitter { 2 | constructor() { 3 | this.listeners = {}; 4 | } 5 | 6 | addEventListener(name, listenerToAdd) { 7 | this.listeners[name] = this.listeners[name] || []; 8 | this.listeners[name].push(listenerToAdd); 9 | 10 | return this; 11 | } 12 | 13 | removeEventListener(name, listenerToRemove) { 14 | if (this.listeners[name] === undefined) { 15 | return; 16 | } 17 | 18 | this.listeners[name] = this.listeners[name].filter((listener) => { 19 | return listener !== listenerToRemove; 20 | }); 21 | 22 | return this; 23 | } 24 | 25 | removeAllEventListeners() { 26 | this.listeners = {}; 27 | } 28 | 29 | emit(name, ...args) { 30 | if (this.listeners[name] === undefined) { 31 | return; 32 | } 33 | 34 | this.listeners[name].forEach((listener) => { 35 | listener(...args); 36 | }); 37 | 38 | return this; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/lib/EventEmitter.test.js: -------------------------------------------------------------------------------- 1 | import avaTest from 'ava'; 2 | import ninos from 'ninos'; 3 | 4 | import { EventEmitter } from './EventEmitter'; 5 | 6 | const test = ninos(avaTest); 7 | 8 | test('multiple events', (t) => { 9 | const emitter = new EventEmitter(); 10 | 11 | const listener1 = t.context.stub(); 12 | const listener2 = t.context.stub(); 13 | 14 | emitter.addEventListener('foo', listener1); 15 | emitter.addEventListener('bar', listener2); 16 | 17 | emitter.emit('foo', 1, '2'); 18 | emitter.emit('bar', 3, '4'); 19 | 20 | t.is(listener1.calls.length, 1); 21 | t.is(listener2.calls.length, 1); 22 | 23 | t.deepEqual(listener1.calls[0].arguments, [1, '2']); 24 | t.deepEqual(listener2.calls[0].arguments, [3, '4']); 25 | }); 26 | 27 | test('multiple listeners', (t) => { 28 | const emitter = new EventEmitter(); 29 | 30 | const listener1 = t.context.stub(); 31 | const listener2 = t.context.stub(); 32 | 33 | emitter.addEventListener('foo', listener1); 34 | emitter.addEventListener('foo', listener2); 35 | 36 | emitter.emit('foo', 1, '2'); 37 | 38 | t.is(listener1.calls.length, 1); 39 | t.is(listener2.calls.length, 1); 40 | 41 | t.deepEqual(listener1.calls[0].arguments, [1, '2']); 42 | t.deepEqual(listener2.calls[0].arguments, [1, '2']); 43 | }); 44 | 45 | test('multiple listeners, remove particular', (t) => { 46 | const emitter = new EventEmitter(); 47 | 48 | const listener1 = t.context.stub(); 49 | const listener2 = t.context.stub(); 50 | 51 | emitter.addEventListener('foo', listener1); 52 | emitter.addEventListener('foo', listener2); 53 | 54 | emitter.removeEventListener('foo', listener1); 55 | 56 | emitter.emit('foo', 1, '2'); 57 | 58 | t.is(listener1.calls.length, 0); 59 | t.is(listener2.calls.length, 1); 60 | 61 | t.deepEqual(listener2.calls[0].arguments, [1, '2']); 62 | }); 63 | -------------------------------------------------------------------------------- /src/lib/FileChunkReader.js: -------------------------------------------------------------------------------- 1 | // Reads a chunk from a file in async/await form 2 | export class FileChunkReader { 3 | constructor(file) { 4 | this.file = file; 5 | } 6 | 7 | async readAsArrayBuffer(start = 0, size = undefined) { 8 | if (size === undefined) { 9 | size = this.file.size - start; 10 | } 11 | 12 | const end = start + size; 13 | 14 | const blob = this.file.slice(start, end); 15 | if (blob.size === 0) { 16 | return new ArrayBuffer(0); 17 | } 18 | 19 | const fileReader = new FileReader(); 20 | 21 | const promise = new Promise((resolve, reject) => { 22 | fileReader.addEventListener('load', (ev) => { 23 | const buffer = ev.target.result; 24 | resolve(buffer); 25 | }); 26 | 27 | fileReader.addEventListener('error', (err) => { 28 | reject(err); 29 | }); 30 | }); 31 | 32 | fileReader.readAsArrayBuffer(blob); 33 | 34 | return promise; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/lib/FileChunkReader.test.js: -------------------------------------------------------------------------------- 1 | import avaTest from 'ava'; 2 | import ninos from 'ninos'; 3 | 4 | import { FileChunkReader } from './FileChunkReader'; 5 | 6 | const test = ninos(avaTest); 7 | 8 | test('empty file', async (t) => { 9 | const data = new Uint8Array([]); 10 | const file = new File([data.buffer], 'test'); 11 | 12 | const reader = new FileChunkReader(file); 13 | 14 | const buffer = await reader.readAsArrayBuffer(); 15 | 16 | t.deepEqual(new Uint8Array(buffer), data); 17 | }); 18 | 19 | test('no args full file', async (t) => { 20 | const data = new Uint8Array([1, 2, 3, 4, 5]); 21 | const file = new File([data.buffer], 'test'); 22 | 23 | const reader = new FileChunkReader(file); 24 | 25 | const buffer = await reader.readAsArrayBuffer(); 26 | 27 | t.deepEqual(new Uint8Array(buffer), data); 28 | }); 29 | 30 | test('read till the end', async (t) => { 31 | const data = new Uint8Array([1, 2, 3, 4, 5]); 32 | const file = new File([data.buffer], 'test'); 33 | 34 | const reader = new FileChunkReader(file); 35 | 36 | const buffer = await reader.readAsArrayBuffer(1); 37 | 38 | t.deepEqual(new Uint8Array(buffer), new Uint8Array([2, 3, 4, 5])); 39 | }); 40 | 41 | test('read chunk', async (t) => { 42 | const data = new Uint8Array([1, 2, 3, 4, 5]); 43 | const file = new File([data.buffer], 'test'); 44 | 45 | const reader = new FileChunkReader(file); 46 | 47 | const buffer = await reader.readAsArrayBuffer(1, 2); 48 | 49 | t.deepEqual(new Uint8Array(buffer), new Uint8Array([2, 3])); 50 | }); 51 | 52 | test('start exceeds length', async (t) => { 53 | const data = new Uint8Array([1, 2, 3, 4, 5]); 54 | const file = new File([data.buffer], 'test'); 55 | 56 | const reader = new FileChunkReader(file); 57 | 58 | const buffer = await reader.readAsArrayBuffer(10); 59 | 60 | t.deepEqual(new Uint8Array(buffer), new Uint8Array([])); 61 | }); 62 | -------------------------------------------------------------------------------- /src/lib/FileReadStream.js: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from './EventEmitter'; 2 | import { FileChunkReader } from './FileChunkReader'; 3 | import threadUtils from '../utils/threadUtils'; 4 | 5 | const DEFAULT_CHUNK_SIZE = 2000000; // 2 MB 6 | const STATE = { 7 | IDLE: 0, 8 | READING: 1, 9 | DESTROYED: 2, 10 | }; 11 | 12 | // Reads entire file in chunks of "chunkSize" size and emits "data" event 13 | // for each chunk. Emits "end" event when done reading. 14 | export class FileReadStream extends EventEmitter { 15 | constructor(file, options = {}) { 16 | super(); 17 | 18 | const { chunkSize = DEFAULT_CHUNK_SIZE } = options; 19 | 20 | this.state = STATE.IDLE; 21 | this.file = file; 22 | this.chunkSize = chunkSize; 23 | this.offset = 0; 24 | this.chunkReader = new FileChunkReader(this.file); 25 | } 26 | 27 | start() { 28 | if (this.state !== STATE.IDLE) { 29 | return; 30 | } 31 | 32 | this.state = STATE.READING; 33 | this.readNextChunk(); 34 | } 35 | 36 | destroy() { 37 | if (this.state === STATE.DESTROYED) { 38 | return; 39 | } 40 | 41 | this.state = STATE.DESTROYED; 42 | this.emit('destroyed'); 43 | this.removeAllEventListeners(); 44 | } 45 | 46 | async readNextChunk() { 47 | if (this.state === STATE.DESTROYED) { 48 | return; 49 | } 50 | 51 | const buffer = await this.chunkReader.readAsArrayBuffer( 52 | this.offset, 53 | this.chunkSize, 54 | ); 55 | 56 | if (buffer.byteLength === 0) { 57 | this.emit('end'); 58 | return; 59 | } 60 | 61 | this.offset += buffer.byteLength; 62 | 63 | this.emit('data', buffer); 64 | 65 | // Don't lock thread with endless processing, work as soon as it is ready 66 | threadUtils.requestIdleCallback(() => { 67 | this.readNextChunk(); 68 | }); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/lib/FileReadStream.test.js: -------------------------------------------------------------------------------- 1 | import avaTest from 'ava'; 2 | import ninos from 'ninos'; 3 | 4 | import { FileReadStream } from './FileReadStream'; 5 | 6 | const test = ninos(avaTest); 7 | 8 | test.cb('empty file', (t) => { 9 | const file = new File([], 'test'); 10 | const stream = new FileReadStream(file); 11 | 12 | const chunkListener = t.context.stub(); 13 | const endListener = t.context.stub(); 14 | 15 | t.plan(1); 16 | 17 | stream.addEventListener('data', chunkListener); 18 | 19 | stream.addEventListener('end', () => { 20 | t.is(chunkListener.calls.length, 0); 21 | t.end(); 22 | }); 23 | 24 | stream.start(); 25 | }); 26 | 27 | test.cb('entire file in one chunk', (t) => { 28 | const data = new Uint8Array([1, 2, 3, 4, 5]); 29 | const file = new File([data.buffer], 'test'); 30 | 31 | const stream = new FileReadStream(file, { 32 | chunkSize: 10, 33 | }); 34 | 35 | const chunkListener = t.context.stub(); 36 | const endListener = t.context.stub(); 37 | 38 | t.plan(2); 39 | 40 | stream.addEventListener('data', chunkListener); 41 | 42 | stream.addEventListener('end', () => { 43 | t.is(chunkListener.calls.length, 1); 44 | t.deepEqual(chunkListener.calls[0].arguments[0], data.buffer); 45 | t.end(); 46 | }); 47 | 48 | stream.start(); 49 | }); 50 | 51 | test.cb('entire file in multiple chunks', (t) => { 52 | const data = new Uint8Array([1, 2, 3, 4, 5]); 53 | const file = new File([data.buffer], 'test'); 54 | 55 | const stream = new FileReadStream(file, { 56 | chunkSize: 1, 57 | }); 58 | 59 | const chunkListener = t.context.stub(); 60 | const endListener = t.context.stub(); 61 | 62 | t.plan(6); 63 | 64 | stream.addEventListener('data', chunkListener); 65 | 66 | stream.addEventListener('end', () => { 67 | t.is(chunkListener.calls.length, 5); 68 | t.deepEqual(chunkListener.calls[0].arguments[0], data.buffer.slice(0, 1)); 69 | t.deepEqual(chunkListener.calls[1].arguments[0], data.buffer.slice(1, 2)); 70 | t.deepEqual(chunkListener.calls[2].arguments[0], data.buffer.slice(2, 3)); 71 | t.deepEqual(chunkListener.calls[3].arguments[0], data.buffer.slice(3, 4)); 72 | t.deepEqual(chunkListener.calls[4].arguments[0], data.buffer.slice(4, 5)); 73 | t.end(); 74 | }); 75 | 76 | stream.start(); 77 | }); 78 | -------------------------------------------------------------------------------- /src/lib/H264BitstreamBinding.js: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from './EventEmitter'; 2 | import { NALU_TYPES } from './H264BitstreamConstants'; 3 | 4 | const MAP_TYPE_TO_METHOD_NAME = { 5 | [NALU_TYPES.PPS]: 'readPPS', 6 | [NALU_TYPES.SPS]: 'readSPS', 7 | [NALU_TYPES.SEI]: 'readSEI', 8 | [NALU_TYPES.AUD]: 'readAUD', 9 | [NALU_TYPES.CODED_SLICE_IDR]: 'readSliceHeader', 10 | [NALU_TYPES.CODED_SLICE_NON_IDR]: 'readSliceHeader', 11 | [NALU_TYPES.CODED_SLICE_AUX]: 'readSliceHeader', 12 | [NALU_TYPES.CODED_SLICE_SVC_EXTENSION]: 'readSliceHeader', 13 | [NALU_TYPES.SUBSET_SPS]: 'readSPSSubset', 14 | [NALU_TYPES.PREFIX_NAL]: 'readPrefixNAL', 15 | }; 16 | 17 | const STATE = { 18 | READY: 0, 19 | DESTROYED: 1, 20 | }; 21 | 22 | export class H264BitstreamBinding extends EventEmitter { 23 | constructor(bitstream) { 24 | super(); 25 | 26 | this.bitstream = bitstream; 27 | 28 | this.binding = new window.Module.Reader(); 29 | this.state = STATE.READY; 30 | } 31 | 32 | destroy() { 33 | if (this.state === STATE.READY) { 34 | this.binding.delete(); 35 | this.binding = null; 36 | } 37 | 38 | this.bitstream = null; 39 | this.state = STATE.DESTROYED; 40 | } 41 | 42 | async read(header) { 43 | const data = await this.bitstream.getUnitData(header); 44 | 45 | let payload = this.createPayload(header); 46 | 47 | if (header.type === NALU_TYPES.SPS) { 48 | payload.sps = this.readByType(header, data); 49 | } else if (header.type === NALU_TYPES.PPS) { 50 | payload.pps = this.readByType(header, data); 51 | } else if (header.type === NALU_TYPES.SEI) { 52 | payload.sei = this.readByType(header, data); 53 | } else if (header.type === NALU_TYPES.AUD) { 54 | payload.aud = this.readByType(header, data); 55 | } else if ( 56 | header.type === NALU_TYPES.CODED_SLICE_IDR || 57 | header.type === NALU_TYPES.CODED_SLICE_NON_IDR || 58 | header.type === NALU_TYPES.CODED_SLICE_AUR 59 | ) { 60 | payload = await this.readSliceHeader(header, data); 61 | } else if (header.type === NALU_TYPES.CODED_SLICE_SVC_EXTENSION) { 62 | payload = await this.readSliceHeader(header, data); 63 | } else if (header.type === NALU_TYPES.SUBSET_SPS) { 64 | payload.sps_subset = await this.readByType(header, data); 65 | } else if (header.type == NALU_TYPES.PREFIX_NAL) { 66 | payload.prefix_nal_svc = await this.readByType(header, data); 67 | } 68 | 69 | if ( 70 | header.type === NALU_TYPES.PREFIX_NAL || 71 | header.type === NALU_TYPES.CODED_SLICE_SVC_EXTENSION || 72 | header.type === 21 73 | ) { 74 | payload.nal_svc_ext = this.readNALHeaderSVCEXT(header, data); 75 | } 76 | 77 | return payload; 78 | } 79 | 80 | async readSliceHeader(header, data) { 81 | const payload = this.createPayload(header); 82 | 83 | // Need to feed pps and sps which are related to this slice header first, 84 | // because info from them in used to calculate slice header info. 85 | const ppsHeader = this.bitstream.findPrecedingHeader( 86 | NALU_TYPES.PPS, 87 | header.start, 88 | ); 89 | if (ppsHeader !== undefined) { 90 | const ppsData = await this.bitstream.getUnitData(ppsHeader); 91 | payload.pps = this.readByType(ppsHeader, ppsData); 92 | } 93 | 94 | const spsHeader = this.bitstream.findPrecedingHeader( 95 | NALU_TYPES.SPS, 96 | header.start, 97 | ); 98 | if (spsHeader !== undefined) { 99 | const spsData = await this.bitstream.getUnitData(spsHeader); 100 | payload.sps = this.readByType(spsHeader, spsData); 101 | } 102 | 103 | payload.sh = this.readByType(header, data); 104 | 105 | return payload; 106 | } 107 | 108 | readNALHeaderSVCEXT(header, data) { 109 | return this.invokeBindingMethod('readNALHeaderSVCEXT', data); 110 | } 111 | 112 | readByType(header, data) { 113 | const methodName = MAP_TYPE_TO_METHOD_NAME[header.type]; 114 | if (methodName === undefined) { 115 | throw new Error(`No method for header type = "${header.type}"`); 116 | } 117 | 118 | return this.invokeBindingMethod(methodName, data); 119 | } 120 | 121 | invokeBindingMethod(methodName, data) { 122 | const numBytes = data.length * data.BYTES_PER_ELEMENT; 123 | const ptr = Module._malloc(numBytes); 124 | 125 | const heapBytes = new Uint8Array(Module.HEAPU8.buffer, ptr, numBytes); 126 | heapBytes.set(new Uint8Array(data.buffer)); 127 | 128 | const ret = this.binding[methodName](ptr, data.length); 129 | 130 | Module._free(ptr); 131 | 132 | return ret; 133 | } 134 | 135 | createPayload(header) { 136 | return { 137 | header, 138 | sps: null, 139 | sps_subset: null, 140 | pps: null, 141 | sh: null, 142 | sei: null, 143 | aud: null, 144 | prefix_nal_svc: null, 145 | nal_svc_ext: null, 146 | }; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/lib/H264BitstreamConstants.js: -------------------------------------------------------------------------------- 1 | export const NALU_TYPES = { 2 | UNSPECIFIED: 0, 3 | CODED_SLICE_NON_IDR: 1, 4 | CODED_SLICE_DATA_PARTITION_A: 2, 5 | CODED_SLICE_DATA_PARTITION_B: 3, 6 | CODED_SLICE_DATA_PARTITION_C: 4, 7 | CODED_SLICE_IDR: 5, 8 | SEI: 6, 9 | SPS: 7, 10 | PPS: 8, 11 | AUD: 9, 12 | END_OF_SEQUENCE: 10, 13 | END_OF_STREAM: 11, 14 | FILLER: 12, 15 | SPS_EXT: 13, 16 | PREFIX_NAL: 14, 17 | SUBSET_SPS: 15, 18 | DPS: 16, 19 | // 17..18 reserved 20 | CODED_SLICE_AUX: 19, 21 | CODED_SLICE_SVC_EXTENSION: 20, 22 | // 20..23 reserved 23 | // 24..31 unspecified 24 | }; 25 | 26 | // https://en.wikipedia.org/wiki/Advanced_Video_Coding#Profiles 27 | export const PROFILE_IDC = { 28 | CAVLC: 44, // CAVLC 4:4:4 Intra Profile (44) 29 | CBP: 66, // Baseline Profile (BP, 66) 30 | MP: 77, // Main Profile (MP, 77) 31 | ScalableBP: 83, // Scalable Baseline Profile (83) 32 | ScalableHP: 86, // Scalable High Profile (86) 33 | XP: 88, // Extended Profile (XP, 88) 34 | HiP: 100, // High Profile (HiP, 100) 35 | Hi10P: 110, // High 10 Profile (Hi10P, 110) 36 | MultiviewHP: 118, // Multiview High Profile (118) 37 | Hi422P: 122, // High 4:2:2 Profile (Hi422P, 122) 38 | StereoHP: 128, // Stereo High Profile (128) 39 | MFCHP: 134, // MFC High Profile (134) 40 | MultiviewDepthHP: 138, // Multiview Depth High Profile (138) 41 | EnchancedMultiviewDepthHP: 139, // Enhanced Multiview Depth High Profile (139) 42 | Hi444PP: 244, // High 4:4:4 Predictive Profile (Hi444PP, 244) 43 | }; 44 | 45 | export const SAR = { 46 | Extended: 255, 47 | }; 48 | 49 | export const SLICE_TYPES = { 50 | P: 0, 51 | B: 1, 52 | I: 2, 53 | EP: 0, 54 | EB: 1, 55 | EI: 2, 56 | SP: 3, 57 | SI: 4, 58 | P_ONLY: 5, 59 | B_ONLY: 6, 60 | I_ONLY: 7, 61 | EP_ONLY: 5, 62 | EB_ONLY: 6, 63 | EI_ONLY: 7, 64 | SP_ONLY: 8, 65 | SI_ONLY: 9, 66 | }; 67 | 68 | export const SEI_TYPES = { 69 | BUFFERING_PERIOD: 0, 70 | PIC_TIMING: 1, 71 | PAN_SCAN_RECT: 2, 72 | FILLER_PAYLOAD: 3, 73 | USER_DATA_REGISTERED_ITU_T_T35: 4, 74 | USER_DATA_UNREGISTERED: 5, 75 | RECOVERY_POINT: 6, 76 | DEC_REF_PIC_MARKING_REPETITION: 7, 77 | SPARE_PIC: 8, 78 | SCENE_INFO: 9, 79 | SUB_SEQ_INFO: 10, 80 | SUB_SEQ_LAYER_CHARACTERISTICS: 11, 81 | SUB_SEQ_CHARACTERISTICS: 12, 82 | FULL_FRAME_FREEZE: 13, 83 | FULL_FRAME_FREEZE_RELEASE: 14, 84 | FULL_FRAME_SNAPSHOT: 15, 85 | PROGRESSIVE_REFINEMENT_SEGMENT_START: 16, 86 | PROGRESSIVE_REFINEMENT_SEGMENT_END: 17, 87 | MOTION_CONSTRAINED_SLICE_GROUP_SET: 18, 88 | FILM_GRAIN_CHARACTERISTICS: 19, 89 | DEBLOCKING_FILTER_DISPLAY_PREFERENCE: 20, 90 | STEREO_VIDEO_INFO: 21, 91 | }; 92 | 93 | export const AUD_PRIMARY_PIC_TYPES = { 94 | I: 0, // I 95 | IP: 1, // I, P 96 | IPB: 2, // I, P, B 97 | SI: 3, // SI 98 | SISP: 4, // SI, SP 99 | ISI: 5, // I, SI 100 | ISIPSP: 6, // I, SI, P, SP 101 | ISIPSPB: 7, // I, SI, P, SP, B 102 | }; 103 | -------------------------------------------------------------------------------- /src/lib/H264BitstreamFile.js: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from './EventEmitter'; 2 | import { FileChunkReader } from './FileChunkReader'; 3 | import { FileReadStream } from './FileReadStream'; 4 | import { H264BitstreamHeaderStream } from './H264BitstreamHeaderStream'; 5 | 6 | export class H264BitstreamFile extends EventEmitter { 7 | constructor(file) { 8 | super(); 9 | 10 | this.file = file; 11 | this.headers = []; 12 | this.lastLoadedOffset = 0; 13 | 14 | this.fileStream = new FileReadStream(this.file); 15 | this.fileStream.addEventListener('data', this.handleFileData.bind(this)); 16 | this.fileStream.addEventListener('end', this.handleFileEnd.bind(this)); 17 | 18 | this.offsetStream = new H264BitstreamHeaderStream(); 19 | this.offsetStream.addEventListener( 20 | 'data', 21 | this.handleOffsetData.bind(this), 22 | ); 23 | this.offsetStream.addEventListener('end', this.handleOffsetEnd.bind(this)); 24 | } 25 | 26 | load() { 27 | this.fileStream.start(); 28 | this.emit('start'); 29 | } 30 | 31 | handleFileData(chunkBuffer) { 32 | this.offsetStream.appendData(new Uint8Array(chunkBuffer)); 33 | } 34 | 35 | handleFileEnd() { 36 | this.offsetStream.finish(); 37 | } 38 | 39 | handleOffsetData(header) { 40 | this.lastLoadedOffset = header.end; 41 | this.headers.push(header); 42 | this.emitProgress(); 43 | } 44 | 45 | handleOffsetEnd() { 46 | this.emitProgress(); 47 | this.emit('end'); 48 | } 49 | 50 | destroy() { 51 | this.file = null; 52 | this.headers = []; 53 | 54 | this.fileStream.destroy(); 55 | this.offsetStream.destroy(); 56 | 57 | this.removeAllEventListeners(); 58 | } 59 | 60 | emitProgress() { 61 | this.emit('progress', this.getProgress()); 62 | } 63 | 64 | findPrecedingHeader(type, maxStartOffset = 0) { 65 | let foundHeader = undefined; 66 | 67 | let currentIndex = 0; 68 | let currentHeader = this.headers[currentIndex]; 69 | 70 | while (currentHeader) { 71 | if (currentHeader.start >= maxStartOffset) { 72 | return foundHeader; 73 | } 74 | 75 | if (currentHeader.type === type) { 76 | foundHeader = currentHeader; 77 | } 78 | 79 | currentIndex++; 80 | currentHeader = this.headers[currentIndex]; 81 | } 82 | 83 | return foundHeader; 84 | } 85 | 86 | getProgress() { 87 | return (this.lastLoadedOffset / this.file.size) * 100; 88 | } 89 | 90 | getTotalHeaders() { 91 | return this.headers.length; 92 | } 93 | 94 | async getUnitData(header) { 95 | const chunkReader = new FileChunkReader(this.file); 96 | const buffer = await chunkReader.readAsArrayBuffer( 97 | header.start, 98 | header.size, 99 | ); 100 | const data = new Uint8Array(buffer); 101 | 102 | const dataNoStartCode = data.slice(3); 103 | 104 | return dataNoStartCode; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/lib/H264BitstreamHeaderStream.js: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from './EventEmitter'; 2 | import { H264BitstreamParser } from './H264BitstreamParser'; 3 | 4 | import arrayUtils from '../utils/arrayUtils'; 5 | 6 | export class H264BitstreamHeaderStream extends EventEmitter { 7 | constructor() { 8 | super(); 9 | 10 | // Data which is left from previous append. 11 | // When #finish() is called, all leftover data will be emitted as the last 12 | // data chunk. 13 | this.leftoverData = new Uint8Array(0); 14 | 15 | // Keep track of offset through all added data, so all emitted offsets 16 | // are aligned to start of the file. 17 | this.thruOffset = 0; 18 | } 19 | 20 | appendData(appendedData) { 21 | const combinedData = arrayUtils.concatUint8Arrays( 22 | this.leftoverData, 23 | appendedData, 24 | ); 25 | 26 | let combinedOffset = 0; 27 | 28 | let unitInfo = H264BitstreamParser.findUnitWithHeader( 29 | combinedData, 30 | combinedOffset, 31 | ); 32 | 33 | // Not able to find any units in appended data, keep it for later appends 34 | if (unitInfo.size === 0) { 35 | this.leftoverData = combinedData; 36 | return; 37 | } 38 | 39 | while (unitInfo.size !== 0) { 40 | // If unit end offset matches combined data length, it should not be 41 | // emitted because we are not sure that unit actually ends here. Next 42 | // append might have extra data for same unit, therefore keep it till 43 | // then. 44 | if (unitInfo.end === combinedData.length - 1) { 45 | this.leftoverData = combinedData.slice(unitInfo.start); 46 | 47 | if (combinedOffset !== 0) { 48 | this.thruOffset += combinedOffset; 49 | } 50 | this.thruOffset += unitInfo.start - combinedOffset; 51 | 52 | return; 53 | } 54 | 55 | this.emitOffset(unitInfo); 56 | 57 | combinedOffset = unitInfo.end; 58 | 59 | unitInfo = H264BitstreamParser.findUnitWithHeader( 60 | combinedData, 61 | combinedOffset, 62 | ); 63 | } 64 | 65 | this.leftoverData = combinedData.slice(combinedOffset + 1); 66 | this.thruOffset += combinedOffset + 1; 67 | } 68 | 69 | destroy() { 70 | this.removeAllEventListeners(); 71 | } 72 | 73 | finish() { 74 | const unitInfo = H264BitstreamParser.findUnitWithHeader(this.leftoverData); 75 | if (unitInfo.size !== 0) { 76 | this.emitOffset(unitInfo); 77 | } 78 | 79 | this.emit('end'); 80 | } 81 | 82 | emitOffset(unitInfo) { 83 | // Apply thru-file offset 84 | const unitInfoWithOffset = { 85 | ...unitInfo, 86 | start: unitInfo.start + this.thruOffset, 87 | end: unitInfo.end + this.thruOffset, 88 | }; 89 | 90 | this.emit('data', unitInfoWithOffset); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/lib/H264BitstreamHeaderStream.test.js: -------------------------------------------------------------------------------- 1 | import avaTest from 'ava'; 2 | import ninos from 'ninos'; 3 | 4 | import { H264BitstreamHeaderStream } from './H264BitstreamHeaderStream'; 5 | 6 | const test = ninos(avaTest); 7 | 8 | test.cb('clean chunks, 3-byte start-code', (t) => { 9 | const stream = new H264BitstreamHeaderStream(); 10 | 11 | const chunkListener = t.context.stub(); 12 | 13 | t.plan(3); 14 | 15 | stream.addEventListener('data', chunkListener); 16 | stream.addEventListener('end', () => { 17 | t.is(chunkListener.calls.length, 2); 18 | t.deepEqual(chunkListener.calls[0].arguments[0], { 19 | start: 0, 20 | end: 4, 21 | size: 5, 22 | forbiddenZeroBit: 0, 23 | refIdc: 3, 24 | type: 7, 25 | }); 26 | t.deepEqual(chunkListener.calls[1].arguments[0], { 27 | start: 5, 28 | end: 10, 29 | size: 6, 30 | forbiddenZeroBit: 0, 31 | refIdc: 3, 32 | type: 8, 33 | }); 34 | t.end(); 35 | }); 36 | 37 | stream.appendData(new Uint8Array([0, 0, 1, 103, 4])); 38 | stream.appendData(new Uint8Array([0, 0, 1, 104, 5, 6])); 39 | stream.finish(); 40 | }); 41 | 42 | test.cb('clean chunks, 4-byte start-code', (t) => { 43 | const stream = new H264BitstreamHeaderStream(); 44 | 45 | const chunkListener = t.context.stub(); 46 | 47 | t.plan(3); 48 | 49 | stream.addEventListener('data', chunkListener); 50 | stream.addEventListener('end', () => { 51 | t.is(chunkListener.calls.length, 2); 52 | t.deepEqual(chunkListener.calls[0].arguments[0], { 53 | start: 1, 54 | end: 5, 55 | size: 5, 56 | forbiddenZeroBit: 0, 57 | refIdc: 3, 58 | type: 7, 59 | }); 60 | t.deepEqual(chunkListener.calls[1].arguments[0], { 61 | start: 7, 62 | end: 12, 63 | size: 6, 64 | forbiddenZeroBit: 0, 65 | refIdc: 3, 66 | type: 8, 67 | }); 68 | t.end(); 69 | }); 70 | 71 | stream.appendData(new Uint8Array([0, 0, 0, 1, 103, 4])); 72 | stream.appendData(new Uint8Array([0, 0, 0, 1, 104, 5, 6])); 73 | stream.finish(); 74 | }); 75 | 76 | test.cb('second chunk zeros in first chunk', (t) => { 77 | const stream = new H264BitstreamHeaderStream(); 78 | 79 | const chunkListener = t.context.stub(); 80 | 81 | t.plan(3); 82 | 83 | stream.addEventListener('data', chunkListener); 84 | stream.addEventListener('end', () => { 85 | t.is(chunkListener.calls.length, 2); 86 | t.deepEqual(chunkListener.calls[0].arguments[0], { 87 | start: 1, 88 | end: 5, 89 | size: 5, 90 | forbiddenZeroBit: 0, 91 | refIdc: 3, 92 | type: 7, 93 | }); 94 | t.deepEqual(chunkListener.calls[1].arguments[0], { 95 | start: 7, 96 | end: 12, 97 | size: 6, 98 | forbiddenZeroBit: 0, 99 | refIdc: 3, 100 | type: 8, 101 | }); 102 | t.end(); 103 | }); 104 | 105 | stream.appendData(new Uint8Array([0, 0, 0, 1, 103, 4, 0, 0, 0])); 106 | stream.appendData(new Uint8Array([1, 104, 5, 6])); 107 | stream.finish(); 108 | }); 109 | 110 | test.cb('combine chunks, 3-byte start-code', (t) => { 111 | const stream = new H264BitstreamHeaderStream(); 112 | 113 | const chunkListener = t.context.stub(); 114 | 115 | t.plan(6); 116 | 117 | stream.addEventListener('data', chunkListener); 118 | stream.addEventListener('end', () => { 119 | t.is(chunkListener.calls.length, 5); 120 | t.deepEqual(chunkListener.calls[0].arguments[0], { 121 | start: 0, 122 | end: 5, 123 | size: 6, 124 | forbiddenZeroBit: 0, 125 | refIdc: 3, 126 | type: 7, 127 | }); 128 | t.deepEqual(chunkListener.calls[1].arguments[0], { 129 | start: 6, 130 | end: 14, 131 | size: 9, 132 | forbiddenZeroBit: 0, 133 | refIdc: 3, 134 | type: 8, 135 | }); 136 | t.deepEqual(chunkListener.calls[2].arguments[0], { 137 | start: 15, 138 | end: 19, 139 | size: 5, 140 | forbiddenZeroBit: 0, 141 | refIdc: 3, 142 | type: 5, 143 | }); 144 | t.deepEqual(chunkListener.calls[3].arguments[0], { 145 | start: 20, 146 | end: 27, 147 | size: 8, 148 | forbiddenZeroBit: 0, 149 | refIdc: 2, 150 | type: 1, 151 | }); 152 | t.deepEqual(chunkListener.calls[4].arguments[0], { 153 | start: 28, 154 | end: 31, 155 | size: 4, 156 | forbiddenZeroBit: 0, 157 | refIdc: 0, 158 | type: 6, 159 | }); 160 | t.end(); 161 | }); 162 | 163 | stream.appendData(new Uint8Array([0, 0, 1, 103, 3, 4, 0, 0, 1, 104])); 164 | stream.appendData(new Uint8Array([6, 7, 8, 9, 10, 0])); 165 | stream.appendData(new Uint8Array([0, 1, 101, 12])); 166 | stream.appendData(new Uint8Array([0, 0, 1, 65])); 167 | stream.appendData(new Uint8Array([14, 15, 16, 17])); 168 | stream.appendData(new Uint8Array([0, 0, 1, 6])); 169 | stream.finish(); 170 | }); 171 | 172 | test.cb('combine chunks, 4-byte start-code', (t) => { 173 | const stream = new H264BitstreamHeaderStream(); 174 | 175 | const chunkListener = t.context.stub(); 176 | 177 | t.plan(7); 178 | 179 | stream.addEventListener('data', chunkListener); 180 | stream.addEventListener('end', () => { 181 | t.is(chunkListener.calls.length, 6); 182 | t.deepEqual(chunkListener.calls[0].arguments[0], { 183 | start: 1, 184 | end: 6, 185 | size: 6, 186 | forbiddenZeroBit: 0, 187 | refIdc: 3, 188 | type: 7, 189 | }); 190 | t.deepEqual(chunkListener.calls[1].arguments[0], { 191 | start: 8, 192 | end: 16, 193 | size: 9, 194 | forbiddenZeroBit: 0, 195 | refIdc: 3, 196 | type: 8, 197 | }); 198 | t.deepEqual(chunkListener.calls[2].arguments[0], { 199 | start: 18, 200 | end: 22, 201 | size: 5, 202 | forbiddenZeroBit: 0, 203 | refIdc: 3, 204 | type: 5, 205 | }); 206 | t.deepEqual(chunkListener.calls[3].arguments[0], { 207 | start: 24, 208 | end: 31, 209 | size: 8, 210 | forbiddenZeroBit: 0, 211 | refIdc: 2, 212 | type: 1, 213 | }); 214 | t.deepEqual(chunkListener.calls[4].arguments[0], { 215 | start: 33, 216 | end: 36, 217 | size: 4, 218 | forbiddenZeroBit: 0, 219 | refIdc: 0, 220 | type: 6, 221 | }); 222 | t.deepEqual(chunkListener.calls[5].arguments[0], { 223 | start: 38, 224 | end: 43, 225 | size: 6, 226 | forbiddenZeroBit: 0, 227 | refIdc: 3, 228 | type: 7, 229 | }); 230 | t.end(); 231 | }); 232 | 233 | stream.appendData(new Uint8Array([0, 0, 0, 1, 103, 3, 4, 0, 0, 0, 1, 104])); 234 | stream.appendData(new Uint8Array([6, 7, 8, 9, 10, 0, 0])); 235 | stream.appendData(new Uint8Array([0, 1, 101, 12])); 236 | stream.appendData(new Uint8Array([0, 0, 0, 1, 65])); 237 | stream.appendData(new Uint8Array([14, 15, 16, 17, 0])); 238 | stream.appendData(new Uint8Array([0, 0, 1, 6, 0, 0, 0])); 239 | stream.appendData(new Uint8Array([1, 103, 20, 21])); 240 | stream.finish(); 241 | }); 242 | -------------------------------------------------------------------------------- /src/lib/H264BitstreamParser.js: -------------------------------------------------------------------------------- 1 | const START_CODE_SHORT = [0, 0, 1]; 2 | const START_CODE_LONG = [0, 0, 0, 1]; 3 | const ZEROS_SHORT = [0, 0, 0]; 4 | 5 | export class H264BitstreamParser { 6 | // Parse H264 bistream according to ISO/IEC 14496 - 10 Annex B, 7 | // but keep the 3-byte start code 8 | static findUnit(data, offset = 0) { 9 | const noResult = { start: -1, end: -1, size: 0 }; 10 | 11 | // Cut off leading zeros and other trash 12 | while (!H264BitstreamParser.isStartCode(data, offset)) { 13 | offset += 1; 14 | 15 | // In case no start code found in entire data 16 | if (offset >= data.length) { 17 | return noResult; 18 | } 19 | } 20 | 21 | if (H264BitstreamParser.isStartCodeLong(data, offset)) { 22 | offset += 1; 23 | } 24 | 25 | // Include 3-byte start-code in result 26 | const start = offset; 27 | 28 | // Skip 3-byte start code 29 | offset += 3; 30 | 31 | // If payload start is in the end of the stream 32 | if (offset === data.length) { 33 | return noResult; 34 | } 35 | 36 | // Move forward until another start code is encountered, preceding it 37 | while ( 38 | !H264BitstreamParser.isZerosShort(data, offset + 1) && 39 | !H264BitstreamParser.isStartCodeShort(data, offset + 1) 40 | ) { 41 | offset += 1; 42 | 43 | // Reached end of data while searching for unit ending. 44 | // Consider unit ends with data. 45 | if (offset >= data.length - 1) { 46 | const end = data.length - 1; 47 | 48 | return { 49 | start, 50 | end, 51 | size: end - start + 1, 52 | }; 53 | } 54 | } 55 | 56 | const end = offset; 57 | 58 | return { 59 | start, 60 | end, 61 | size: end - start + 1, 62 | }; 63 | } 64 | 65 | static findUnitWithHeader(data, offset = 0) { 66 | const location = this.findUnit(data, offset); 67 | 68 | const headerOffset = location.start; 69 | const header = this.readUnitHeader(data, headerOffset); 70 | 71 | return { 72 | ...location, 73 | ...header, 74 | }; 75 | } 76 | 77 | static readUnitHeader(data, offset = 0) { 78 | const noHeader = { 79 | forbiddenZeroBit: -1, 80 | refIdc: -1, 81 | type: -1, 82 | }; 83 | 84 | if (!H264BitstreamParser.isStartCode(data, offset)) { 85 | return noHeader; 86 | } 87 | 88 | const headerOffset = offset + START_CODE_SHORT.length; 89 | 90 | if (data.length - 1 < headerOffset) { 91 | return noHeader; 92 | } 93 | 94 | const header = data[headerOffset]; 95 | const forbiddenZeroBit = header >> 7; 96 | const refIdc = (header >> 5) & 0x3; 97 | const type = header & 0x1f; 98 | 99 | return { 100 | forbiddenZeroBit, 101 | refIdc, 102 | type, 103 | }; 104 | } 105 | 106 | static isStartCodeShort(data, offset = 0) { 107 | return ( 108 | data[offset] === START_CODE_SHORT[0] && 109 | data[offset + 1] === START_CODE_SHORT[1] && 110 | data[offset + 2] === START_CODE_SHORT[2] 111 | ); 112 | } 113 | 114 | static isStartCodeLong(data, offset = 0) { 115 | return ( 116 | data[offset] === START_CODE_LONG[0] && 117 | data[offset + 1] === START_CODE_LONG[1] && 118 | data[offset + 2] === START_CODE_LONG[2] && 119 | data[offset + 3] === START_CODE_LONG[3] 120 | ); 121 | } 122 | 123 | static isStartCode(data, offset = 0) { 124 | return ( 125 | H264BitstreamParser.isStartCodeShort(data, offset) || 126 | H264BitstreamParser.isStartCodeLong(data, offset) 127 | ); 128 | } 129 | 130 | static isZerosShort(data, offset = 0) { 131 | return ( 132 | data[offset] === ZEROS_SHORT[0] && 133 | data[offset + 1] === ZEROS_SHORT[1] && 134 | data[offset + 2] === ZEROS_SHORT[2] 135 | ); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/lib/H264BitstreamParser.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | 3 | import { H264BitstreamParser } from './H264BitstreamParser'; 4 | 5 | test('findUnit: empty data, no offset', (t) => { 6 | const data = new Uint8Array(0); 7 | 8 | const result = H264BitstreamParser.findUnit(data); 9 | 10 | t.deepEqual(result, { start: -1, end: -1, size: 0 }); 11 | }); 12 | 13 | test('findUnit: no start-codes', (t) => { 14 | const data = new Uint8Array([1, 2, 3, 4, 5]); 15 | 16 | const result = H264BitstreamParser.findUnit(data); 17 | 18 | t.deepEqual(result, { start: -1, end: -1, size: 0 }); 19 | }); 20 | 21 | test('findUnit: 3-byte start-code in the end', (t) => { 22 | const data = new Uint8Array([1, 2, 3, 0, 0, 1]); 23 | 24 | const result = H264BitstreamParser.findUnit(data); 25 | 26 | t.deepEqual(result, { start: -1, end: -1, size: 0 }); 27 | }); 28 | 29 | test('findUnit: 4-byte start-code in the end', (t) => { 30 | const data = new Uint8Array([1, 2, 3, 0, 0, 0, 1]); 31 | 32 | const result = H264BitstreamParser.findUnit(data); 33 | 34 | t.deepEqual(result, { start: -1, end: -1, size: 0 }); 35 | }); 36 | 37 | test('findUnit: single unit, leading zeros', (t) => { 38 | const data = new Uint8Array([0, 0, 0, 0, 0, 0, 1, 5, 6]); 39 | 40 | const result = H264BitstreamParser.findUnit(data); 41 | 42 | t.deepEqual(result, { start: 4, end: 8, size: 5 }); 43 | }); 44 | 45 | test('findUnit: single unit, leading trash', (t) => { 46 | const data = new Uint8Array([1, 2, 3, 4, 0, 0, 1, 5, 6]); 47 | 48 | const result = H264BitstreamParser.findUnit(data); 49 | 50 | t.deepEqual(result, { start: 4, end: 8, size: 5 }); 51 | }); 52 | 53 | test('findUnit: two units, 3-byte start-codes', (t) => { 54 | const data = new Uint8Array([0, 0, 1, 5, 6, 0, 0, 1, 7, 8, 9]); 55 | 56 | const result1 = H264BitstreamParser.findUnit(data, 0); 57 | const result2 = H264BitstreamParser.findUnit(data, 4); 58 | 59 | t.deepEqual(result1, { start: 0, end: 4, size: 5 }); 60 | t.deepEqual(result2, { start: 5, end: 10, size: 6 }); 61 | }); 62 | 63 | test('findUnit: two units, second one has only header', (t) => { 64 | const data = new Uint8Array([0, 0, 1, 5, 6, 0, 0, 1, 5]); 65 | 66 | const result1 = H264BitstreamParser.findUnit(data, 0); 67 | const result2 = H264BitstreamParser.findUnit(data, 4); 68 | 69 | t.deepEqual(result1, { start: 0, end: 4, size: 5 }); 70 | t.deepEqual(result2, { start: 5, end: 8, size: 4 }); 71 | }); 72 | 73 | test('findUnit: two units, second one with 4-byte start-codes', (t) => { 74 | const data = new Uint8Array([0, 0, 1, 5, 6, 0, 0, 0, 1, 7, 8, 9]); 75 | 76 | const result1 = H264BitstreamParser.findUnit(data, 0); 77 | const result2 = H264BitstreamParser.findUnit(data, 4); 78 | 79 | t.deepEqual(result1, { start: 0, end: 4, size: 5 }); 80 | t.deepEqual(result2, { start: 6, end: 11, size: 6 }); 81 | }); 82 | 83 | test('findUnit: two units, all with 4-byte start-codes', (t) => { 84 | const data = new Uint8Array([0, 0, 0, 1, 5, 6, 0, 0, 0, 1, 7, 8, 9]); 85 | 86 | const result1 = H264BitstreamParser.findUnit(data, 0); 87 | const result2 = H264BitstreamParser.findUnit(data, 5); 88 | 89 | t.deepEqual(result1, { start: 1, end: 5, size: 5 }); 90 | t.deepEqual(result2, { start: 7, end: 12, size: 6 }); 91 | }); 92 | 93 | test('findUnitWithHeader: no start-codes', (t) => { 94 | const data = new Uint8Array([1, 2, 3, 4, 5]); 95 | 96 | const result = H264BitstreamParser.findUnitWithHeader(data); 97 | 98 | t.deepEqual(result, { 99 | start: -1, 100 | end: -1, 101 | size: 0, 102 | forbiddenZeroBit: -1, 103 | refIdc: -1, 104 | type: -1, 105 | }); 106 | }); 107 | 108 | test('findUnitWithHeader: single unit, leading zeros', (t) => { 109 | const data = new Uint8Array([0, 0, 0, 0, 0, 0, 1, 103, 6]); 110 | 111 | const result = H264BitstreamParser.findUnitWithHeader(data); 112 | 113 | t.deepEqual(result, { 114 | start: 4, 115 | end: 8, 116 | size: 5, 117 | forbiddenZeroBit: 0, 118 | refIdc: 3, 119 | type: 7, 120 | }); 121 | }); 122 | 123 | test('findUnitWithHeader: single unit, leading trash', (t) => { 124 | const data = new Uint8Array([1, 2, 3, 4, 0, 0, 1, 6, 7]); 125 | 126 | const result = H264BitstreamParser.findUnitWithHeader(data); 127 | 128 | t.deepEqual(result, { 129 | start: 4, 130 | end: 8, 131 | size: 5, 132 | forbiddenZeroBit: 0, 133 | refIdc: 0, 134 | type: 6, 135 | }); 136 | }); 137 | 138 | test('readUnitHeader: empty data', (t) => { 139 | const header = H264BitstreamParser.readUnitHeader(new Uint8Array([])); 140 | 141 | t.deepEqual(header, { 142 | forbiddenZeroBit: -1, 143 | refIdc: -1, 144 | type: -1, 145 | }); 146 | }); 147 | 148 | test('readUnitHeader: different units', (t) => { 149 | const header1 = H264BitstreamParser.readUnitHeader( 150 | new Uint8Array([0, 0, 1, 6]), 151 | ); 152 | const header2 = H264BitstreamParser.readUnitHeader( 153 | new Uint8Array([0, 0, 1, 103]), 154 | ); 155 | const header3 = H264BitstreamParser.readUnitHeader( 156 | new Uint8Array([0, 0, 1, 104]), 157 | ); 158 | const header4 = H264BitstreamParser.readUnitHeader( 159 | new Uint8Array([0, 0, 1, 101]), 160 | ); 161 | const header5 = H264BitstreamParser.readUnitHeader( 162 | new Uint8Array([0, 0, 1, 65]), 163 | ); 164 | 165 | t.deepEqual(header1, { 166 | forbiddenZeroBit: 0, 167 | refIdc: 0, 168 | type: 6, 169 | }); 170 | t.deepEqual(header2, { 171 | forbiddenZeroBit: 0, 172 | refIdc: 3, 173 | type: 7, 174 | }); 175 | t.deepEqual(header3, { 176 | forbiddenZeroBit: 0, 177 | refIdc: 3, 178 | type: 8, 179 | }); 180 | t.deepEqual(header4, { 181 | forbiddenZeroBit: 0, 182 | refIdc: 3, 183 | type: 5, 184 | }); 185 | t.deepEqual(header5, { 186 | forbiddenZeroBit: 0, 187 | refIdc: 2, 188 | type: 1, 189 | }); 190 | }); 191 | 192 | test('readUnitHeader: offset on start-code', (t) => { 193 | const header = H264BitstreamParser.readUnitHeader( 194 | new Uint8Array([1, 2, 3, 0, 0, 1, 6]), 195 | 3, 196 | ); 197 | 198 | t.deepEqual(header, { 199 | forbiddenZeroBit: 0, 200 | refIdc: 0, 201 | type: 6, 202 | }); 203 | }); 204 | 205 | test('readUnitHeader: offset before start-code', (t) => { 206 | const header = H264BitstreamParser.readUnitHeader( 207 | new Uint8Array([1, 2, 3, 0, 0, 1, 6]), 208 | 2, 209 | ); 210 | 211 | t.deepEqual(header, { 212 | forbiddenZeroBit: -1, 213 | refIdc: -1, 214 | type: -1, 215 | }); 216 | }); 217 | -------------------------------------------------------------------------------- /src/lib/KeyboardListener.js: -------------------------------------------------------------------------------- 1 | export const KEY_CODES = { 2 | ARROW_LEFT: 37, 3 | ARROW_UP: 38, 4 | ARROW_RIGHT: 39, 5 | ARROW_DOWN: 40, 6 | I: 73, 7 | P: 80, 8 | ENTER: 13, 9 | }; 10 | 11 | export const makeKeydownListener = (keyCode, listener = () => {}) => { 12 | document.addEventListener('keydown', (ev) => { 13 | if (ev.keyCode !== keyCode) { 14 | return; 15 | } 16 | 17 | listener(ev); 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /src/lib/index.js: -------------------------------------------------------------------------------- 1 | export * from './EventEmitter'; 2 | export * from './FileChunkReader'; 3 | export * from './FileReadStream'; 4 | export * from './H264BitstreamBinding'; 5 | export * from './H264BitstreamConstants'; 6 | export * from './H264BitstreamFile'; 7 | export * from './H264BitstreamHeaderStream'; 8 | export * from './H264BitstreamParser'; 9 | export * from './KeyboardListener'; 10 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | import 'keen-ui/dist/keen-ui.css'; 4 | 5 | import './assets/styles/global.css'; 6 | 7 | import App from './components/App'; 8 | 9 | import h264Helper from './helpers/h264Helper'; 10 | 11 | Vue.filter('naluTypeDisplayed', h264Helper.naluTypeDisplayed); 12 | Vue.filter('seiTypeDisplayed', h264Helper.seiTypeDisplayed); 13 | Vue.filter('audPrimaryPicTypeDisplayed', h264Helper.audPrimaryPicTypeDisplayed); 14 | 15 | const app = new Vue({ 16 | render: (h) => h(App), 17 | }); 18 | 19 | app.$mount('[data-app]'); 20 | -------------------------------------------------------------------------------- /src/utils/arrayUtils.js: -------------------------------------------------------------------------------- 1 | const arrayUtils = { 2 | range(start = 0, end = undefined) { 3 | if (end === undefined) { 4 | end = start; 5 | start = 0; 6 | } 7 | 8 | const items = []; 9 | 10 | for (let i = start; i < end; i += 1) { 11 | items.push(i); 12 | } 13 | 14 | return items; 15 | }, 16 | 17 | concatUint8Arrays(...arrays) { 18 | const size = arrays.reduce((sum, arr) => { 19 | return sum + arr.length; 20 | }, 0); 21 | 22 | const result = new Uint8Array(size); 23 | 24 | let offset = 0; 25 | 26 | arrays.forEach((array) => { 27 | result.set(array, offset); 28 | offset += array.length; 29 | }); 30 | 31 | return result; 32 | }, 33 | }; 34 | 35 | export default arrayUtils; 36 | -------------------------------------------------------------------------------- /src/utils/arrayUtils.test.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | 3 | import arrayUtils from './arrayUtils'; 4 | 5 | test('range: empty', (t) => { 6 | const range = arrayUtils.range(); 7 | 8 | t.deepEqual(range, []); 9 | }); 10 | 11 | test('range: no start', (t) => { 12 | const range = arrayUtils.range(3); 13 | 14 | t.deepEqual(range, [0, 1, 2]); 15 | }); 16 | 17 | test('range: start and end', (t) => { 18 | const range = arrayUtils.range(2, 5); 19 | 20 | t.deepEqual(range, [2, 3, 4]); 21 | }); 22 | 23 | test('concatUint8Arrays: empty', (t) => { 24 | const result = arrayUtils.concatUint8Arrays(); 25 | 26 | t.deepEqual(result, new Uint8Array([])); 27 | }); 28 | 29 | test('concatUint8Arrays: one input', (t) => { 30 | const input1 = new Uint8Array([1, 2, 3]); 31 | 32 | const result = arrayUtils.concatUint8Arrays(input1); 33 | 34 | t.deepEqual(result, input1); 35 | t.not(result, input1); 36 | }); 37 | 38 | test('concatUint8Arrays: two inputs', (t) => { 39 | const input1 = new Uint8Array([1, 2, 3]); 40 | const input2 = new Uint8Array([4, 5]); 41 | 42 | const result = arrayUtils.concatUint8Arrays(input1, input2); 43 | 44 | t.deepEqual(result, new Uint8Array([1, 2, 3, 4, 5])); 45 | }); 46 | -------------------------------------------------------------------------------- /src/utils/threadUtils.js: -------------------------------------------------------------------------------- 1 | const threadUtils = { 2 | // https://developers.google.com/web/updates/2015/08/using-requestidlecallback 3 | // Safari does not have it yet. Also used when runinng units tests because 4 | // Node does not have it either. 5 | requestIdleCallback: window.requestIdleCallback 6 | ? window.requestIdleCallback.bind(window) 7 | : function(cb) { 8 | const start = Date.now(); 9 | return setTimeout(function() { 10 | cb({ 11 | didTimeout: false, 12 | timeRemaining: function() { 13 | return Math.max(0, 50 - (Date.now() - start)); 14 | }, 15 | }); 16 | }, 1); 17 | }, 18 | }; 19 | 20 | export default threadUtils; 21 | -------------------------------------------------------------------------------- /test/helpers/setupBrowserEnv.js: -------------------------------------------------------------------------------- 1 | // https://github.com/avajs/ava/blob/master/docs/recipes/browser-testing.md 2 | 3 | import browserEnv from 'browser-env'; 4 | 5 | browserEnv(); 6 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const HTMLWebpackPlugin = require('html-webpack-plugin'); 4 | const VueLoaderPlugin = require('vue-loader/lib/plugin'); 5 | 6 | const rootDir = path.join(__dirname); 7 | 8 | const config = { 9 | mode: 'development', 10 | entry: { 11 | main: path.join(rootDir, 'src/main.js'), 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.vue$/, 17 | loader: 'vue-loader', 18 | }, 19 | // This will apply to both plain `.css` files and `