daio 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
daio-0.0.1/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+
2
+ The MIT License (MIT)
3
+
4
+ Copyright (c) jlab.berlin
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in
14
+ all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
+ THE SOFTWARE.
daio-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,39 @@
1
+ Metadata-Version: 2.1
2
+ Name: daio
3
+ Version: 0.0.1
4
+ Summary: video and data IO tools for Python
5
+ Author: jlab.berlin
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/danionella/daio
8
+ Keywords: python,HDF5,data IO,video IO
9
+ Requires-Python: >=3.8
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: numpy
13
+ Requires-Dist: tqdm
14
+ Requires-Dist: av
15
+ Requires-Dist: h5py
16
+ Requires-Dist: hdf5plugin
17
+
18
+ ![Python Version](https://img.shields.io/badge/python-3.8+-blue)
19
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
20
+ ![tests](https://github.com/danionella/daio/actions/workflows/test.yml/badge.svg)
21
+ [![PyPI - Version](https://img.shields.io/pypi/v/daio)](https://pypi.org/project/daio/)
22
+ [![Conda Version](https://img.shields.io/conda/v/danionella/daio)](https://anaconda.org/danionella/daio)
23
+ ![GitHub last commit](https://img.shields.io/github/last-commit/danionella/daio)
24
+
25
+ # daio
26
+ Video and data IO tools for Python.
27
+
28
+ Links: [API documentation](http://danionella.github.io/my_package), [GitHub repository](https://github.com/danionella/my_package)
29
+
30
+ ## Installation
31
+ - via conda or mamba: `conda install danionella::daio `
32
+ - if you prefer pip: `pip install daio`
33
+ - for development, clone this repository change to the directory containing `pyproject.toml`
34
+ - `conda env create -n env_name -f environment.yml`
35
+ - `conda activate env_name`
36
+ - `pip install -e .`
37
+
38
+ ## Use
39
+ TODO.
daio-0.0.1/README.md ADDED
@@ -0,0 +1,22 @@
1
+ ![Python Version](https://img.shields.io/badge/python-3.8+-blue)
2
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
3
+ ![tests](https://github.com/danionella/daio/actions/workflows/test.yml/badge.svg)
4
+ [![PyPI - Version](https://img.shields.io/pypi/v/daio)](https://pypi.org/project/daio/)
5
+ [![Conda Version](https://img.shields.io/conda/v/danionella/daio)](https://anaconda.org/danionella/daio)
6
+ ![GitHub last commit](https://img.shields.io/github/last-commit/danionella/daio)
7
+
8
+ # daio
9
+ Video and data IO tools for Python.
10
+
11
+ Links: [API documentation](http://danionella.github.io/my_package), [GitHub repository](https://github.com/danionella/my_package)
12
+
13
+ ## Installation
14
+ - via conda or mamba: `conda install danionella::daio `
15
+ - if you prefer pip: `pip install daio`
16
+ - for development, clone this repository change to the directory containing `pyproject.toml`
17
+ - `conda env create -n env_name -f environment.yml`
18
+ - `conda activate env_name`
19
+ - `pip install -e .`
20
+
21
+ ## Use
22
+ TODO.
@@ -0,0 +1,30 @@
1
+ [build-system]
2
+ requires = ["setuptools>=42", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "daio"
7
+ version = "0.0.1"
8
+ description = "video and data IO tools for Python"
9
+ authors = [
10
+ { name = "jlab.berlin" },
11
+ ]
12
+ license = {text = "MIT"}
13
+ readme = "README.md"
14
+ keywords = ["python", "HDF5", "data IO", "video IO"]
15
+ requires-python = ">=3.8"
16
+ dependencies = [
17
+ "numpy","tqdm","av","h5py","hdf5plugin"
18
+ ]
19
+
20
+ [project.urls]
21
+ Homepage = "https://github.com/danionella/daio"
22
+
23
+ [tool.setuptools.packages.find]
24
+ where = ["src"]
25
+ include = ["daio*"]
26
+
27
+ [tool.pytest.ini_options]
28
+ addopts = [
29
+ "--import-mode=importlib",
30
+ ]
daio-0.0.1/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,172 @@
1
+ import json
2
+ #TODO: test replacing json with orjson
3
+ import warnings
4
+ from os.path import expanduser
5
+
6
+ import numpy as np
7
+ import h5py
8
+ #import hdf5plugin
9
+ #TODO: test defaulting to hdf5plugin.Zstd compression for json-serialized strings
10
+
11
+
12
+ def save_to_h5(filename, data, serialize=True, compression=None, json_compression='gzip', verbosity=1, file_mode='w', convert_numpy_to_native=False):
13
+ ''' Save a nested dictionary data structure to an HDF5 file.
14
+
15
+ Args:
16
+ filename (string): file name of the HDF5 file
17
+ data (dict): Nested dictionary whose contents may be dict, ndarray, str, bytes, DataFrame and JSON-serializable objects
18
+ serialize (boolean): enable JSON serialization
19
+ compression (string): h5py compression type (e.g. 'gzip', 'lzf' or None)
20
+ json_compression (string): h5py compression type for serialized JSON (default: 'gzip')
21
+ file_mode (string): h5py.File access mode. 'w' (default) for create/detete and 'a' for create/append
22
+
23
+ based on https://github.com/danionella/lib2p/blob/master/lib2putils.py
24
+ '''
25
+
26
+ def recursively_save_contents_to_group(h5file, path, data_item):
27
+ assert isinstance(data_item, (dict))
28
+ for key, item in data_item.items():
29
+ if verbosity > 1:
30
+ print('saving entry: {} -- {}'.format(path + key, type(item)))
31
+ if isinstance(item, (np.ndarray, np.int64, np.float64, str, bytes, int, float)):
32
+ comp = None if np.isscalar(item) else compression
33
+ try:
34
+ h5file[path].create_dataset(key, data=item, compression=comp)
35
+ except TypeError:
36
+ warnings.warn(f'\n\nkey: {key} -- Saving data with compression failed. Saving without compression.\n')
37
+ h5file[path].create_dataset(key, data=item, compression=None)
38
+ elif type(item).__name__ == 'DataFrame':
39
+ json_bytes = np.frombuffer(item.to_json().encode('utf-8'), dtype='byte')
40
+ h5file[path].create_dataset(key, data=json_bytes, compression=json_compression)
41
+ h5file[path + key].attrs['pandas_json_type'] = f'This {type(item)} was JSON serialized and UTF-8 encoded.'
42
+ elif isinstance(item, dict):
43
+ h5file[path].create_group(key)
44
+ recursively_save_contents_to_group(h5file, path + key + '/', item)
45
+ elif serialize:
46
+ if verbosity > 0:
47
+ print(f'serializing {type(item)} at {path+key}', flush=True)
48
+ #TODO: test replacing json with orjson
49
+ json_bytes = json.dumps(item).encode('utf-8')
50
+ h5file[path].create_dataset(key, data=np.frombuffer(json_bytes, dtype='byte'), compression=json_compression)
51
+ h5file[path + key].attrs['json_type'] = f'This {type(item)} was JSON serialized and UTF-8 encoded.'
52
+ else:
53
+ raise ValueError(f'Cannot save {type(item)} to {path+key}. Consider enabling serialisation.')
54
+
55
+ if convert_numpy_to_native:
56
+ data = convert_numpy_to_native(data)
57
+
58
+ filename = expanduser(filename)
59
+ with h5py.File(filename, file_mode) as h5file:
60
+ recursively_save_contents_to_group(h5file, '/', data)
61
+
62
+
63
+ def load_from_h5(filename):
64
+ ''' Load an HDF5 file to a dictionary
65
+
66
+ Args:
67
+ filename (string): file name of the HDF5 file
68
+
69
+ Returns:
70
+ dict: file contents
71
+ '''
72
+
73
+ def recursively_load_contents_from_group(h5file, path):
74
+ ans = dict()
75
+ for key, item in h5file[path].items():
76
+ if 'pandas_type' in item.attrs.keys():
77
+ import pandas as pd
78
+ ans[key] = pd.read_hdf(filename, path + key)
79
+ elif 'pandas_json_type' in item.attrs.keys():
80
+ import pandas as pd
81
+ json_str = item[()].tobytes().decode('utf-8')
82
+ ans[key] = pd.read_json(json_str)
83
+ elif 'json_type' in item.attrs.keys():
84
+ #TODO: test replacing json with orjson
85
+ ans[key] = json.loads(item[()].tobytes())
86
+ elif isinstance(item, h5py._hl.dataset.Dataset):
87
+ if h5py.check_string_dtype(item.dtype) is not None:
88
+ item = item.asstr()
89
+ ans[key] = item[()]
90
+ elif isinstance(item, h5py._hl.group.Group):
91
+ ans[key] = recursively_load_contents_from_group(h5file, path + key + '/')
92
+ else:
93
+ raise ValueError(f"I don't know what to do about {path+key}.")
94
+ return ans
95
+
96
+ filename = expanduser(filename)
97
+ with h5py.File(filename, 'r') as h5file:
98
+ return recursively_load_contents_from_group(h5file, '/')
99
+
100
+
101
+ class lazyh5:
102
+ """ A lazy-loading interface for HDF5 files.
103
+
104
+ This class provides an easy way to access HDF5 file content without fully
105
+ loading it into memory. It supports dynamic access to datasets and subgroups.
106
+
107
+ Args:
108
+ filepath (str): Path to the HDF5 file.
109
+ h5path (str, optional): HDF5 group path. Defaults to '/'.
110
+ overwrite (bool, optional): Whether to overwrite existing items. Defaults to False.
111
+ """
112
+
113
+ def __init__(self, filepath, h5path='/', overwrite=False):
114
+ self._filepath = filepath
115
+ self._h5path = h5path
116
+ self._overwrite = overwrite
117
+
118
+ def keys(self):
119
+ """Lists the keys of the current HDF5 group.
120
+
121
+ Returns:
122
+ list: List of keys in the current HDF5 group.
123
+ """
124
+ with h5py.File(self._filepath, 'r') as f:
125
+ return list(f[self._h5path].keys())
126
+
127
+ def __getitem__(self, key):
128
+ """Gets an item by key."""
129
+ with h5py.File(self._filepath, 'r') as f:
130
+ item = f[self._h5path][key]
131
+ if isinstance(item, h5py.Group):
132
+ return lazyh5(self._filepath, h5path=f"{self._h5path}/{key}")
133
+ elif isinstance(item, h5py.Dataset):
134
+ return item[()]
135
+ else:
136
+ raise KeyError(f"Unknown item type for key: {key}")
137
+
138
+ def __getattr__(self, key):
139
+ """Provides dynamic attribute-style access."""
140
+ try:
141
+ return self[key]
142
+ except KeyError:
143
+ raise AttributeError(key)
144
+
145
+ def __setattr__(self, key, value):
146
+ """Sets an attribute or creates a new dataset."""
147
+ if key.startswith('_'):
148
+ super().__setattr__(key, value)
149
+ return
150
+
151
+ with h5py.File(self._filepath, 'a') as f:
152
+ full_path = f"{self._h5path}/{key}".lstrip('/')
153
+ if (full_path in f) and (not self._overwrite):
154
+ raise AttributeError(f"Dataset or group '{key}' already exists.")
155
+ else:
156
+ f[self._h5path].create_dataset(key, data=value)
157
+
158
+ def __len__(self):
159
+ """Gets the number of items in the current HDF5 group."""
160
+ return len(self.keys())
161
+
162
+ def __repr__(self):
163
+ """Provides a string representation of the object."""
164
+ return f"<lazyh5 for file '{self._filepath}', HDF5 path '{self._h5path}' with {len(self)} items>"
165
+
166
+ def _ipython_key_completions_(self):
167
+ """Enables key completions in IPython."""
168
+ return self.keys()
169
+
170
+ def __dir__(self):
171
+ """Lists all accessible attributes and keys."""
172
+ return self.keys() + dir(super())
@@ -0,0 +1,265 @@
1
+ import warnings
2
+
3
+ import numpy as np
4
+ import av
5
+
6
+ warnings.simplefilter("once")
7
+
8
+ class VideoReader:
9
+ ''' A class to read video files quickly, using PyAV. It allows for fast seeking and reading of frames.
10
+
11
+ Args:
12
+ filename (str): path to the video file
13
+ format (str): format to read the video in. Default is 'gray'. Other options include 'rgb24', 'yuv420p', etc.
14
+ threading (bool): enable threading in the decoder. Default is True.
15
+ thread_count (int): number of threads to use for decoding. Default is 0 (auto).
16
+ tolerance (float): tolerance for frame time stamp deviations (fraction of frame interval). Default is 0.1.
17
+ '''
18
+ def __init__(self, filename, format='gray', threading=True, thread_count=0, tolerance=0.1):
19
+ self.container = av.open(filename)
20
+ self.stream = self.container.streams.video[0]
21
+ self.stream.codec_context.thread_count = thread_count
22
+ self.stream.codec_context.thread_type = 'AUTO' if threading else 'SLICE' # FRAME/AUTO/SLICE
23
+ self.framegenerator = self.container.decode(video=0)
24
+ self.read_format = format
25
+ self._pts_per_frame = 1 / (self.stream.guessed_rate * self.stream.time_base)
26
+ self._frame_to_pts = lambda n: round(n * self._pts_per_frame) + self.stream.start_time
27
+ self.wiggle_pts = tolerance * self._pts_per_frame
28
+ self.rewind()
29
+ if self.container.format.variable_fps:
30
+ warn_transcode(f'Variable frame rate video detected. Seeking will likely be unrealiable. I will warn again if I detect seek gitches')
31
+ if self.stream.average_rate != self.stream.guessed_rate:
32
+ warn_transcode(f'Average frame rate ({self.stream.average_rate}) is different from nominal frame rate ({self.stream.guessed_rate}). Seeking may be unrealiable. I will warn again if I detect seek gitches')
33
+
34
+ def read(self):
35
+ ''' Read the next frame in the specified format. '''
36
+ frame_obj = next(self.framegenerator)
37
+ self.last_pts = frame_obj.pts
38
+ im = frame_obj.to_ndarray(format=self.read_format)
39
+ return im
40
+
41
+ def rewind(self):
42
+ ''' Rewind the video to the beginning. '''
43
+ self.container.seek(0)
44
+ self.framegenerator = self.container.decode(video=0)
45
+ self.last_pts = None
46
+
47
+ def read_frame(self, frame_idx):
48
+ ''' Read the specified frame index.
49
+
50
+ Args:
51
+ frame_idx (int): index of the frame to read
52
+ '''
53
+ if frame_idx == 0:
54
+ self.rewind()
55
+ return self.read()
56
+ if self.last_pts is not None and (np.abs(self.last_pts - self._frame_to_pts(frame_idx-1)) <= self.wiggle_pts):
57
+ return self.read()
58
+ target_pts = self._frame_to_pts(frame_idx)
59
+ self.container.seek(target_pts-self.stream.start_time, backward=True, stream=self.container.streams.video[0])
60
+ self.framegenerator = self.container.decode(video=0)
61
+ frame_obj = next(self.framegenerator)
62
+ if frame_obj.pts > target_pts: #detecting overshoot (may happen due to variable frame rate)
63
+ n_back = 100
64
+ warn_transcode(f'Seek overshoot ({frame_obj.pts} > {target_pts}). Backtracking by {n_back} frames...')
65
+ self.container.seek(self._frame_to_pts(frame_idx-n_back)-self.stream.start_time, backward=True, stream=self.container.streams.video[0])
66
+ self.framegenerator = self.container.decode(video=0)
67
+ frame_obj = next(self.framegenerator)
68
+ while frame_obj.pts < (target_pts - self.wiggle_pts):
69
+ frame_obj = next(self.framegenerator)
70
+ # frame_obj.pts should now be equal to target_pts
71
+ if np.abs(frame_obj.pts - target_pts) > self.wiggle_pts:
72
+ warn_transcode(f'Seek problem with frame {frame_idx}! pts: {frame_obj.pts}; target: {target_pts}; dts: {frame_obj.dts}; pict_type: {str(frame_obj.pict_type)}')
73
+ frame = frame_obj.to_ndarray(format=self.read_format)
74
+ self.last_pts = frame_obj.pts
75
+ return frame
76
+
77
+ def close(self):
78
+ self.container.close()
79
+
80
+ def __del__(self):
81
+ self.close()
82
+
83
+ def __getitem__(self, index):
84
+ if isinstance(index, (int, np.integer)): # single frame
85
+ return self.read_frame(index)
86
+ elif isinstance(index, slice):
87
+ frames = [self.__getitem__(i) for i in np.r_[index]]
88
+ return np.array(frames)
89
+ elif isinstance(index, tuple):
90
+ frames = [self.__getitem__(i)[index[1:]] for i in np.r_[index[0]]]
91
+ if len(frames) == 1:
92
+ return frames[0]
93
+ else:
94
+ return np.array(frames)
95
+ else:
96
+ raise NotImplementedError(f"slicing of {type(index)} : {index} not implemented yet")
97
+
98
+ def __enter__(self):
99
+ return self
100
+
101
+ def __exit__(self, exc_type, exc_value, traceback):
102
+ self.close()
103
+ return False
104
+
105
+ def __iter__(self):
106
+ self.rewind()
107
+ return self
108
+
109
+ def __next__(self):
110
+ try:
111
+ return self.read()
112
+ except:
113
+ self.rewind()
114
+ raise StopIteration
115
+
116
+ @property
117
+ def frame_shape(self):
118
+ ''' Return the shape of the video frames. '''
119
+ return self.container.streams.video[0].codec_context.height, self.container.streams.video[0].codec_context.width
120
+
121
+ @property
122
+ def nframes(self):
123
+ ''' Return the number of frames in the video. '''
124
+ return self.container.streams.video[0].frames
125
+
126
+ @property
127
+ def dtype(self):
128
+ return np.uint8
129
+
130
+ @property
131
+ def shape(self):
132
+ if self.read_format == 'gray':
133
+ return (self.nframes, *self.frame_shape)
134
+ elif self.read_format in ['rgb24', 'bgr24', 'yuv420p']:
135
+ return (self.nframes, *self.frame_shape, 3)
136
+ else:
137
+ raise NotImplementedError(f"shape not implemented for format {self.read_format}")
138
+
139
+ @property
140
+ def ndim(self):
141
+ return len(self.shape)
142
+
143
+ @property
144
+ def size(self):
145
+ return np.product(self.shape)
146
+
147
+ @staticmethod
148
+ def static_shape(filename):
149
+ ''' Get the shape of a video (static method).
150
+
151
+ Args:
152
+ filename (str): path to the video file'''
153
+ with av.open(filename) as container:
154
+ stream = container.streams.video[0]
155
+ shape = np.array([stream.frames, stream.codec_context.height, stream.codec_context.width])
156
+ return shape
157
+
158
+
159
+ def warn_transcode(msg):
160
+ warnings.warn(msg, stacklevel=2)
161
+ warnings.warn('Consider transcoding (ffmpeg -y -i "input.mp4" -c:v libx264 -pix_fmt yuv420p -preset superfast -crf 23 "output.mp4").', stacklevel=2)
162
+
163
+
164
+ class VideoWriter:
165
+ ''' A class to write video files, using PyAV.
166
+
167
+ Args:
168
+ filename (str): path to the output video file
169
+ codec (str): codec to use. Default is 'libx264'. Other options include 'h264', etc. Check VideoWriter.codecs_available for a list of available codecs.
170
+ fps (int): frames per second. Default is 25.
171
+ frame_shape (tuple): shape of the frames. Default is None (to be determined by the first frame written).
172
+ **kwargs: additional keyword arguments to pass to the codec. Check e.g. `ffmpeg -h encoder=libx264`
173
+ '''
174
+
175
+ def __init__(self, filename, codec='libx264', fps=25, frame_shape=None, pix_fmt='yuv420p', **kwargs):
176
+ self.container = av.open(filename, mode='w')
177
+ #define codec defaults:
178
+ if codec == 'h264':
179
+ kwargs = dict(bit_rate=1000000, pix_fmt='yuv420p').update(kwargs)
180
+ elif codec == 'libx264':
181
+ kwargs = dict(crf=23, preset='superfast', pix_fmt='yuv420p').update(kwargs)
182
+ #add video stream, if frame_shape is provided (otherwise create when the first frame is written)
183
+ if frame_shape is not None:
184
+ self.stream = self.container.add_stream(codec, rate=fps, height=frame_shape[0], width=frame_shape[1], pix_fmt=pix_fmt, **kwargs)
185
+ else:
186
+ self.stream = None
187
+ self.codec = codec
188
+ self.fps = fps
189
+ self.pix_fmt = pix_fmt
190
+ self.kwargs = kwargs if kwargs is not None else {}
191
+
192
+ def write(self, im):
193
+ """ Write a frame to the video file
194
+
195
+ Args:
196
+ im (np.ndarray): frame to write
197
+ """
198
+ if im.ndim == 2:
199
+ format = 'gray'
200
+ elif im.ndim == 3 and im.shape[2] == 3:
201
+ format = 'rgb24'
202
+ else:
203
+ raise ValueError(f"Unsupported frame shape: {im.shape}")
204
+ if self.stream is None:
205
+ self.stream = self.container.add_stream(self.codec, rate=self.fps, height=im.shape[0], width=im.shape[1], pix_fmt=self.pix_fmt, **self.kwargs)
206
+ out_frame = av.VideoFrame.from_ndarray(im, format=format)
207
+ for packet in self.stream.encode(out_frame):
208
+ self.container.mux(packet)
209
+
210
+ def write_frames(self, frames):
211
+ for frame in frames:
212
+ self.write(frame)
213
+
214
+ def write_audio(self, audio_chunk, audio_rate, pts=None, stream_args=None):
215
+ """ Write an audio chunk to the video file
216
+
217
+ Args:
218
+ audio_chunk (np.ndarray): audio chunk to write
219
+ audio_rate (int): audio sample rate
220
+ pts (int): presentation timestamp of the audio chunk. Default is None (auto-increment)
221
+ stream_args (dict): additional arguments to pass to the audio stream.
222
+ """
223
+ if stream_args is None:
224
+ stream_args = dict(codec_name='aac', rate=44100)
225
+ if self.audio_stream is None:
226
+ self.audio_stream = self.container.add_stream(**stream_args)
227
+ self.channel_layout = 'stereo' if (audio_chunk.ndim > 1 and audio_chunk.shape[0] == 2) else 'mono'
228
+ self.audio_pts = 0
229
+ audio_frame = av.AudioFrame.from_ndarray(audio_chunk, layout=self.channel_layout)
230
+ audio_frame.sample_rate = audio_rate
231
+ audio_frame.pts = self.audio_pts if pts is None else pts
232
+ self.audio_pts = audio_frame.pts + audio_chunk.shape[1]
233
+ for packet in self.audio_stream.encode(audio_frame):
234
+ self.container.mux(packet)
235
+
236
+ def close(self):
237
+ """ Close the video file. """
238
+ try:
239
+ for packet in self.stream.encode():
240
+ self.container.mux(packet)
241
+ except:
242
+ pass
243
+
244
+ try:
245
+ for packet in self.audio_stream.encode():
246
+ self.container.mux(packet)
247
+ except:
248
+ pass
249
+
250
+ self.container.close()
251
+
252
+ def __del__(self):
253
+ self.close()
254
+
255
+ def __enter__(self):
256
+ return self
257
+
258
+ def __exit__(self, exc_type, exc_value, traceback):
259
+ self.close()
260
+ return False
261
+
262
+ @property
263
+ def codecs_available(self):
264
+ ''' Return a list of available codecs. '''
265
+ return sorted(av.codec.codecs_available)
@@ -0,0 +1,39 @@
1
+ Metadata-Version: 2.1
2
+ Name: daio
3
+ Version: 0.0.1
4
+ Summary: video and data IO tools for Python
5
+ Author: jlab.berlin
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/danionella/daio
8
+ Keywords: python,HDF5,data IO,video IO
9
+ Requires-Python: >=3.8
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: numpy
13
+ Requires-Dist: tqdm
14
+ Requires-Dist: av
15
+ Requires-Dist: h5py
16
+ Requires-Dist: hdf5plugin
17
+
18
+ ![Python Version](https://img.shields.io/badge/python-3.8+-blue)
19
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
20
+ ![tests](https://github.com/danionella/daio/actions/workflows/test.yml/badge.svg)
21
+ [![PyPI - Version](https://img.shields.io/pypi/v/daio)](https://pypi.org/project/daio/)
22
+ [![Conda Version](https://img.shields.io/conda/v/danionella/daio)](https://anaconda.org/danionella/daio)
23
+ ![GitHub last commit](https://img.shields.io/github/last-commit/danionella/daio)
24
+
25
+ # daio
26
+ Video and data IO tools for Python.
27
+
28
+ Links: [API documentation](http://danionella.github.io/my_package), [GitHub repository](https://github.com/danionella/my_package)
29
+
30
+ ## Installation
31
+ - via conda or mamba: `conda install danionella::daio `
32
+ - if you prefer pip: `pip install daio`
33
+ - for development, clone this repository change to the directory containing `pyproject.toml`
34
+ - `conda env create -n env_name -f environment.yml`
35
+ - `conda activate env_name`
36
+ - `pip install -e .`
37
+
38
+ ## Use
39
+ TODO.
@@ -0,0 +1,11 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/daio/h5.py
5
+ src/daio/video.py
6
+ src/daio.egg-info/PKG-INFO
7
+ src/daio.egg-info/SOURCES.txt
8
+ src/daio.egg-info/dependency_links.txt
9
+ src/daio.egg-info/requires.txt
10
+ src/daio.egg-info/top_level.txt
11
+ tests/test_all.py
@@ -0,0 +1,5 @@
1
+ numpy
2
+ tqdm
3
+ av
4
+ h5py
5
+ hdf5plugin
@@ -0,0 +1 @@
1
+ daio
@@ -0,0 +1,8 @@
1
+ # Run: pytest tests/test_all.py
2
+ # All functions to be tested should start with test_ prefix
3
+
4
+ def test_trivial():
5
+ assert True == True
6
+
7
+ def test_trivial2():
8
+ assert False == False